Entropic 2.3.8
Local-first agentic inference engine
Loading...
Searching...
No Matches
constitutional_validator.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
14
16
17#include <cstdlib>
18#include <cstring>
19
20namespace entropic {
21
22namespace {
23auto logger = entropic::log::get("core.constitutional_validator");
24} // anonymous namespace
25
26// Forward declarations for static helpers used in validate()
27static std::string strip_think_blocks(const std::string& content);
28static bool is_pure_tool_call(const std::string& content);
29
38 const std::string& constitution_text)
39 : config_(config),
40 constitution_text_(constitution_text),
41 global_enabled_(config.enabled) {
42 context_.validator = this;
43 context_.inference = nullptr;
44}
45
55 HookInterface* hook_iface,
56 InferenceInterface* inference) {
57 inference_ = inference;
58 context_.inference = inference;
59
60 if (hook_iface == nullptr || hook_iface->registry == nullptr) {
62 }
63
64 auto* reg = static_cast<HookRegistry*>(hook_iface->registry);
65 return reg->register_hook(
68 &context_,
69 config_.priority);
70}
71
78void ConstitutionalValidator::detach(HookInterface* hook_iface) {
79 if (hook_iface == nullptr || hook_iface->registry == nullptr) {
80 return;
81 }
82 auto* reg = static_cast<HookRegistry*>(hook_iface->registry);
83 reg->deregister_hook(
85}
86
100 const std::string& identity_name) const {
101 std::lock_guard<std::mutex> lock(overrides_mutex_);
102 auto it = identity_overrides_.find(identity_name);
103 if (it != identity_overrides_.end()) {
104 return it->second;
105 }
106 // Default skip for tiers that stream before the hook fires
107 for (const auto& skip : config_.skip_tiers) {
108 if (skip == identity_name) { return false; }
109 }
110 return global_enabled_;
111}
112
120 std::lock_guard<std::mutex> lock(overrides_mutex_);
121 global_enabled_ = enabled;
122}
123
124// ── gh#30 (v2.1.5): consumer-driven retry controls ───────
125
133 auto_retry_enabled_.store(enabled);
134}
135
143 return auto_retry_enabled_.load();
144}
145
158 std::optional<PendingValidationState> state;
159 {
160 std::lock_guard lock(pending_mutex_);
161 state = std::move(pending_state_);
162 pending_state_.reset();
163 }
164 if (!state) {
166 }
167 auto result = apply_revisions(
168 state->result, state->critique,
169 state->messages_json.empty() ? nullptr
170 : state->messages_json.c_str());
171 store_result(result);
172 logger->info("Constitutional validation resumed (gh#30): "
173 "final verdict={}", static_cast<int>(result.verdict));
174 return ENTROPIC_OK;
175}
176
188 std::optional<PendingValidationState> state;
189 {
190 std::lock_guard lock(pending_mutex_);
191 state = std::move(pending_state_);
192 pending_state_.reset();
193 }
194 if (!state) {
196 }
197 ValidationResult result = std::move(state->result);
199 store_result(result);
200 logger->info("Constitutional validation accepted by consumer "
201 "(gh#30): attempt_n={}", result.attempt_n);
202 return ENTROPIC_OK;
203}
204
213 void (*cb)(int, void*), void* user_data) {
214 std::lock_guard<std::mutex> lock(attempt_boundary_mutex_);
215 attempt_boundary_.cb = cb;
216 attempt_boundary_.user_data = user_data;
217}
218
234 void (*start_cb)(void*),
235 void (*end_cb)(void*),
236 void* user_data) {
237 std::lock_guard<std::mutex> lock(critique_cbs_mutex_);
238 critique_cbs_.start_cb = start_cb;
239 critique_cbs_.end_cb = end_cb;
240 critique_cbs_.user_data = user_data;
241}
242
251 const std::string& identity_name, bool enabled) {
252 std::lock_guard<std::mutex> lock(overrides_mutex_);
253 identity_overrides_[identity_name] = enabled;
254}
255
264 const std::string& identity_name,
265 const std::vector<std::string>& rules) {
266 std::lock_guard<std::mutex> lock(overrides_mutex_);
267 tier_rules_[identity_name] = rules;
268}
269
286 const std::string& content,
287 const std::string& tier,
288 const char* messages_json) {
289 ValidationResult result;
290 result.content = content;
291
292 if (!should_validate(tier)) {
293 logger->info("Validation skipped for tier '{}'", tier);
295 store_result(result);
296 return result;
297 }
298
299 // Strip think blocks and skip pure tool-call outputs
300 auto cleaned = strip_think_blocks(content);
301 if (cleaned.empty() || is_pure_tool_call(cleaned)) {
302 logger->info("Validation skipped: pure tool-call or empty");
304 store_result(result);
305 return result;
306 }
307
308 current_tier_ = tier;
309 logger->info("Validation start: {} chars, tier='{}'",
310 cleaned.size(), tier);
311 result = run_validation_loop(cleaned, tier, messages_json);
312 log_verdict(result);
313 store_result(result);
314 return result;
315}
316
323void ConstitutionalValidator::log_verdict(
324 const ValidationResult& result) const {
325 switch (result.verdict) {
327 logger->info("Validation passed (no violations)");
328 break;
330 logger->info("Validation revised ({} revision(s) applied)",
331 result.revision_count);
332 break;
334 logger->warn("Validation reverted "
335 "({} violation(s) found; revision discarded for length)",
336 result.final_critique.violations.size());
337 break;
339 logger->warn("Validation rejected "
340 "(max revisions exhausted; {} violation(s) remain)",
341 result.final_critique.violations.size());
342 break;
344 // log-site above already emits "Validation skipped…"
345 break;
347 logger->info("Validation paused (gh#30): "
348 "{} violation(s); awaiting consumer",
349 result.final_critique.violations.size());
350 break;
352 logger->info("Validation accepted by consumer (gh#30): "
353 "attempt_n={}", result.attempt_n);
354 break;
355 }
356}
357
365 std::lock_guard<std::mutex> lock(result_mutex_);
366 return last_result_;
367}
368
369// ── Hook Callback ────────────────────────────────────────
370
382 entropic_hook_point_t /*hook_point*/,
383 const char* context_json,
384 char** modified_json,
385 void* user_data) {
386 *modified_json = nullptr;
387 auto* ctx = static_cast<ValidationContext*>(user_data);
388 if (ctx == nullptr || ctx->validator == nullptr) {
389 return 0;
390 }
391
392 return ctx->validator->handle_hook(context_json, modified_json);
393}
394
395// ── Critique Prompt Assembly ──────────────────────────────
396
419 const std::string& content) const {
420 std::string prompt;
421 prompt.reserve(constitution_text_.size() + content.size() + 512);
422
423 prompt += "You are a compliance evaluator. "
424 "Respond ONLY with the structured JSON evaluation.\n\n";
425
426 // When per-tier rules exist: they are the primary rubric,
427 // constitution is background context. When absent: constitution
428 // is the sole rubric.
429 bool has_tier_rules = false;
430 {
431 std::lock_guard<std::mutex> lock(overrides_mutex_);
432 auto it = tier_rules_.find(current_tier_);
433 has_tier_rules = (it != tier_rules_.end()
434 && !it->second.empty());
435 if (has_tier_rules) {
436 prompt += "Evaluate against these rules for the '"
437 + current_tier_ + "' identity:\n";
438 for (const auto& rule : it->second) {
439 prompt += "- " + rule + "\n";
440 }
441 prompt += "\nBackground constitutional guidance:\n";
442 prompt += constitution_text_;
443 } else {
444 prompt += "Constitutional Rules:\n";
445 prompt += constitution_text_;
446 }
447 }
448
449 // Provide tool call manifest so validator can assess grounding
450 if (!current_tool_context_.empty()) {
451 prompt += "\n\nTool calls made this turn:\n";
452 prompt += current_tool_context_;
453 }
454
455 // Issue #5 (v2.1.3): un-pruned tool-result content. Lets the
456 // critique pass verify file:line citations against actual
457 // evidence rather than the manifest-plus-stubs that pre-2.1.3
458 // produced. Engine surfaces this when the messages have been
459 // partially pruned (#6 limits when this happens, but legitimate
460 // long-context delegations still trigger it).
461 if (!current_tool_evidence_.empty()) {
462 prompt += "\n\nTool result evidence (verify citations against this):\n";
463 prompt += current_tool_evidence_;
464 }
465
466 prompt += "\n\nEvaluate this output for compliance:\n\n---\n";
467 prompt += content;
468 prompt += "\n---";
469 return prompt;
470}
471
481 const std::string& json_str) {
482 CritiqueResult result;
483 result.raw_json = json_str;
484
485 if (!extract_compliant_field(json_str, result)) {
486 // Fail-open: if the model produced JSON we can't parse (e.g.,
487 // "constitutional_compliance_status" instead of "compliant"),
488 // treat it as compliant rather than triggering a revision loop
489 // on a parse error. The grammar constraint is the real fix;
490 // this is the safety net when grammar loading fails.
491 result.compliant = true;
492 return result;
493 }
494
495 extract_violations(json_str, result);
496 extract_revised_field(json_str, result);
497 return result;
498}
499
500// ── Private Implementation ────────────────────────────────
501
508void ConstitutionalValidator::store_result(
509 const ValidationResult& result) {
510 std::lock_guard<std::mutex> lock(result_mutex_);
511 last_result_ = result;
512}
513
523ValidationResult ConstitutionalValidator::run_validation_loop(
524 const std::string& content,
525 const std::string& tier,
526 const char* messages_json) {
527 ValidationResult result;
528 result.content = content;
529
530 auto critique = run_critique(content);
531 result.final_critique = critique;
532
533 if (critique.compliant) {
534 return result;
535 }
536
537 // gh#30 (v2.1.5): when the consumer has disabled auto-retry, stop
538 // here and stash enough state for resume_retry()/accept_last() to
539 // continue.
540 if (!auto_retry_enabled_.load()) {
542 result.attempt_n = 0;
543 {
544 std::lock_guard lock(pending_mutex_);
545 pending_state_ = PendingValidationState{
546 result, critique,
547 messages_json ? std::string(messages_json) : std::string{},
548 tier};
549 }
550 logger->info("Constitutional validation paused (gh#30): "
551 "auto_retry disabled, awaiting consumer decision");
552 return result;
553 }
554
555 return apply_revisions(result, critique, messages_json);
556}
557
575ValidationResult ConstitutionalValidator::apply_revisions(
576 ValidationResult result,
577 const CritiqueResult& initial_critique,
578 const char* messages_json) {
579 auto critique = initial_critique;
580 for (int i = 0; i < config_.max_revisions; ++i) {
581 const auto& before = result.content;
582 // gh#30 (v2.1.5): fire attempt-boundary callback before the
583 // revision so consumers can split rendered output cleanly.
584 // Snapshot under the mutex first so a concurrent
585 // set_attempt_boundary_cb() cannot tear the {cb, user_data}
586 // pair mid-call (post-2.1.5 verification hardening).
587 AttemptBoundaryCb cb_snap;
588 {
589 std::lock_guard<std::mutex> lk(attempt_boundary_mutex_);
590 cb_snap = attempt_boundary_;
591 }
592 if (cb_snap.cb != nullptr) {
593 try {
594 cb_snap.cb(i + 1, cb_snap.user_data);
595 } catch (...) {
596 logger->warn("attempt_boundary_cb threw; swallowed at "
597 ".so boundary (gh#30)");
598 }
599 }
600 auto revised = attempt_revision(before, critique, messages_json);
601
602 // Length safety valve: reject revisions that gut the content
603 if (revised.size() < before.size() / 2) {
604 logger->warn("Constitutional validation: revision {}/{} "
605 "shrank content {}→{} chars (>50%); "
606 "discarding revision, returning original",
607 i + 1, config_.max_revisions,
608 before.size(), revised.size());
609 result.verdict =
611 result.attempt_n = i + 1;
612 return result;
613 }
614
615 result.content = revised;
616 result.was_revised = true;
617 result.revision_count = i + 1;
618 result.attempt_n = i + 1;
619
620 critique = run_critique(revised);
621 result.final_critique = critique;
622
623 if (critique.compliant) { break; }
624 }
625
626 if (!critique.compliant) {
627 logger->warn("Constitutional validation: max revisions ({}) "
628 "exhausted, returning last output",
629 config_.max_revisions);
631 } else if (result.was_revised) {
632 result.verdict = ValidationVerdict::revised;
633 }
634 return result;
635}
636
646std::string ConstitutionalValidator::attempt_revision(
647 const std::string& content,
648 const CritiqueResult& critique,
649 const char* messages_json) {
650 if (!critique.revised.empty()) {
651 return critique.revised;
652 }
653 return revise(content, critique, messages_json);
654}
655
663CritiqueResult ConstitutionalValidator::run_critique(
664 const std::string& content) {
665 if (inference_ == nullptr || inference_->generate == nullptr) {
666 logger->warn("Constitutional validation: no inference "
667 "interface, skipping critique");
668 return {};
669 }
670
671 auto messages = build_critique_messages(content);
672 auto params = build_critique_params();
673 char* result_json = nullptr;
674
675 // gh#50 (v2.1.12): snapshot the callback pair under the mutex
676 // so a consumer reassigning the slot mid-critique cannot tear
677 // {start_cb, end_cb, user_data}. Invoke OUTSIDE the lock so a
678 // pathological consumer-side handler that re-enters
679 // set_critique_callbacks doesn't self-deadlock.
680 CritiqueCallbacks cbs;
681 {
682 std::lock_guard<std::mutex> lock(critique_cbs_mutex_);
683 cbs = critique_cbs_;
684 }
685 if (cbs.start_cb != nullptr) { cbs.start_cb(cbs.user_data); }
686
687 int rc = inference_->generate(
688 messages.c_str(), params.c_str(),
689 &result_json, inference_->backend_data);
690
691 if (cbs.end_cb != nullptr) { cbs.end_cb(cbs.user_data); }
692
693 if (rc != 0 || result_json == nullptr) {
694 logger->warn("Constitutional validation: critique generation "
695 "failed (rc={})", rc);
696 return {};
697 }
698
699 std::string raw(result_json);
700 if (inference_->free_fn != nullptr) {
701 inference_->free_fn(result_json);
702 }
703
704 return parse_critique(raw);
705}
706
714std::string ConstitutionalValidator::build_critique_messages(
715 const std::string& content) const {
716 auto prompt = build_critique_prompt(content);
717 return build_single_turn_json(prompt);
718}
719
726std::string ConstitutionalValidator::build_critique_params() const {
727 std::string params = "{\"grammar_key\":\"";
728 params += config_.grammar_key;
729 params += "\",\"max_tokens\":";
730 params += std::to_string(config_.max_critique_tokens);
731 params += ",\"temperature\":";
732 params += std::to_string(config_.temperature);
733 params += ",\"enable_thinking\":";
734 params += config_.enable_thinking ? "true" : "false";
735 // E4 (2.0.6-rc17): route critique on a dedicated (typically
736 // smaller) tier so grammar-constrained sampling doesn't burn
737 // 35B primary inference. Empty -> default_tier selected by
738 // the inference interface.
739 if (!config_.critique_tier.empty()) {
740 params += ",\"tier\":\"";
741 params += config_.critique_tier;
742 params += "\"";
743 }
744 params += "}";
745 return params;
746}
747
764std::string ConstitutionalValidator::revise(
765 const std::string& original,
766 const CritiqueResult& critique,
767 const char* messages_json) {
768 if (inference_ == nullptr || inference_->generate == nullptr) {
769 return original;
770 }
771
772 auto augmented = build_revision_messages(
773 original, critique, messages_json);
774 char* result_json = nullptr;
775
776 // Unconstrained generation: revision output is free-form prose,
777 // not the structured JSON schema used for the critique pass.
778 int rc = inference_->generate(
779 augmented.c_str(), "{}", &result_json,
780 inference_->backend_data);
781
782 if (rc != 0 || result_json == nullptr) {
783 logger->warn("Constitutional validation: revision generation "
784 "failed (rc={})", rc);
785 return original;
786 }
787
788 std::string revised(result_json);
789 if (inference_->free_fn != nullptr) {
790 inference_->free_fn(result_json);
791 }
792 return revised;
793}
794
795// ── JSON String Helpers ──────────────────────────────────
796// Manual JSON construction avoids nlohmann/json dependency in core.so.
797
805static std::string json_escape(const std::string& s) {
806 std::string out;
807 out.reserve(s.size() + 16);
808 for (char c : s) {
809 switch (c) {
810 case '"': out += "\\\""; break;
811 case '\\': out += "\\\\"; break;
812 case '\n': out += "\\n"; break;
813 case '\r': out += "\\r"; break;
814 case '\t': out += "\\t"; break;
815 default: out += c; break;
816 }
817 }
818 return out;
819}
820
828std::string ConstitutionalValidator::build_single_turn_json(
829 const std::string& prompt) const {
830 std::string json = "[{\"role\":\"user\",\"content\":\"";
831 json += json_escape(prompt);
832 json += "\"}]";
833 return json;
834}
835
845std::string ConstitutionalValidator::build_revision_messages(
846 const std::string& original,
847 const CritiqueResult& critique,
848 const char* messages_json) const {
849 std::string feedback = build_feedback_text(critique);
850 return inject_feedback_into_messages(
851 original, feedback, messages_json);
852}
853
861std::string ConstitutionalValidator::build_feedback_text(
862 const CritiqueResult& critique) const {
863 std::string feedback =
864 "Your response violated these constitutional rules:\\n";
865 for (const auto& v : critique.violations) {
866 feedback += "- " + v.rule + ": " + v.explanation + "\\n";
867 }
868 feedback += "Please revise your response to comply with all "
869 "constitutional rules.";
870 return feedback;
871}
872
888std::string ConstitutionalValidator::inject_feedback_into_messages(
889 const std::string& original,
890 const std::string& feedback,
891 const char* messages_json) const {
892 std::string base;
893 if (messages_json != nullptr) {
894 base = std::string(messages_json);
895 } else {
896 // Prefer identity system prompt over bare constitution — keeps
897 // the model in persona during revision (prevents apology spirals).
898 const auto& sys = !current_system_prompt_.empty()
899 ? current_system_prompt_ : constitution_text_;
900 base = "[{\"role\":\"system\",\"content\":\""
901 + json_escape(sys) + "\"}";
902 }
903
904 // Strip trailing ]
905 if (!base.empty() && base.back() == ']') {
906 base.pop_back();
907 }
908
909 // Add comma if there were existing messages
910 if (base.size() > 1) {
911 base += ",";
912 }
913
914 base += "{\"role\":\"assistant\",\"content\":\"";
915 base += json_escape(original);
916 base += "\"},{\"role\":\"system\",\"content\":\"";
917 base += json_escape("[CONSTITUTIONAL REVIEW] " + feedback);
918 base += "\"}]";
919 return base;
920}
921
934static std::string strip_think_blocks(const std::string& content) {
935 std::string result;
936 size_t pos = 0;
937 while (pos < content.size()) {
938 auto open = content.find("<think>", pos);
939 if (open == std::string::npos) {
940 result.append(content, pos);
941 break;
942 }
943 result.append(content, pos, open - pos);
944 auto close = content.find("</think>", open);
945 pos = (close == std::string::npos)
946 ? content.size()
947 : close + 8;
948 }
949 return result;
950}
951
964static bool is_pure_tool_call(const std::string& content) {
965 std::string stripped = content;
966 // Remove all <tool_call>...</tool_call> blocks
967 while (true) {
968 auto open = stripped.find("<tool_call>");
969 if (open == std::string::npos) { break; }
970 auto close = stripped.find("</tool_call>", open);
971 if (close == std::string::npos) { break; }
972 stripped.erase(open, close + 12 - open);
973 }
974 // If only whitespace remains, it was a pure tool call
975 return stripped.find_first_not_of(" \t\n\r") == std::string::npos;
976}
977
992int ConstitutionalValidator::handle_hook(
993 const char* context_json,
994 char** modified_json) {
995 *modified_json = nullptr;
996
997 auto content = extract_json_string(context_json, "content");
998 auto tier = extract_json_string(context_json, "tier");
999
1000 if (content.empty()) { return 0; }
1001
1002 // Store per-call context for build_critique_prompt and revision
1003 current_tool_context_ = extract_json_string(
1004 context_json, "tool_context");
1005 // Issue #5 (v2.1.3): un-pruned tool-result content surfaced by the
1006 // engine so the validator can verify citations against actual
1007 // evidence rather than post-prune stubs. Optional field — pre-2.1.3
1008 // engines that don't send it give an empty string, falling back to
1009 // the manifest-only behaviour.
1010 current_tool_evidence_ = extract_json_string(
1011 context_json, "tool_evidence");
1012 current_system_prompt_ = extract_json_string(
1013 context_json, "system_prompt");
1014
1015 auto result = validate(content, tier, nullptr);
1016 if (!result.was_revised) {
1017 return 0;
1018 }
1019
1020 write_modified_json(result.content, modified_json);
1021 return 0;
1022}
1023
1031void ConstitutionalValidator::write_modified_json(
1032 const std::string& content,
1033 char** modified_json) {
1034 std::string out = "{\"content\":\"";
1035 out += json_escape(content);
1036 out += "\"}";
1037
1038 auto* buf = static_cast<char*>(malloc(out.size() + 1));
1039 if (buf != nullptr) {
1040 std::memcpy(buf, out.c_str(), out.size() + 1);
1041 *modified_json = buf;
1042 }
1043}
1044
1045// ── Minimal JSON Field Extraction ─────────────────────────
1046// No nlohmann/json in core.so. These extract known fields from
1047// simple flat JSON objects by string search.
1048
1057std::string ConstitutionalValidator::extract_json_string(
1058 const char* json, const char* key) {
1059 if (json == nullptr || key == nullptr) {
1060 return {};
1061 }
1062
1063 std::string needle = std::string("\"") + key + "\"";
1064 const char* pos = strstr(json, needle.c_str());
1065 if (pos == nullptr) {
1066 return {};
1067 }
1068
1069 return extract_string_after_colon(pos + needle.size());
1070}
1071
1079std::string ConstitutionalValidator::extract_string_after_colon(
1080 const char* pos) {
1081 // Skip whitespace and colon
1082 while (*pos == ' ' || *pos == ':' || *pos == '\t') {
1083 ++pos;
1084 }
1085 if (*pos != '"') {
1086 return {};
1087 }
1088 ++pos; // skip opening quote
1089
1090 std::string value;
1091 while (*pos != '\0' && *pos != '"') {
1092 if (*pos == '\\' && *(pos + 1) != '\0') {
1093 ++pos;
1094 switch (*pos) {
1095 case 'n': value += '\n'; break;
1096 case 't': value += '\t'; break;
1097 case 'r': value += '\r'; break;
1098 default: value += *pos; break;
1099 }
1100 } else {
1101 value += *pos;
1102 }
1103 ++pos;
1104 }
1105 return value;
1106}
1107
1116bool ConstitutionalValidator::extract_compliant_field(
1117 const std::string& json, CritiqueResult& result) {
1118 auto pos = json.find("\"compliant\"");
1119 if (pos == std::string::npos) {
1120 logger->warn("Constitutional validation: malformed critique "
1121 "JSON — missing 'compliant' field");
1122 return false;
1123 }
1124
1125 result.compliant = (json.find("true", pos) < json.find("false", pos));
1126 return true;
1127}
1128
1136void ConstitutionalValidator::extract_violations(
1137 const std::string& json, CritiqueResult& result) {
1138 size_t search_pos = 0;
1139
1140 while (true) {
1141 auto v = extract_next_violation(json, search_pos);
1142 if (!v.has_value()) {
1143 break;
1144 }
1145 result.violations.push_back(std::move(v.value()));
1146 }
1147}
1148
1157std::optional<Violation>
1158ConstitutionalValidator::extract_next_violation(
1159 const std::string& json, size_t& pos) {
1160 auto rule_pos = json.find("\"rule\"", pos);
1161 if (rule_pos == std::string::npos) {
1162 return std::nullopt;
1163 }
1164
1165 Violation v;
1166 v.rule = extract_json_string(
1167 json.c_str() + rule_pos, "rule");
1168 v.excerpt = extract_json_string(
1169 json.c_str() + rule_pos, "excerpt");
1170 v.explanation = extract_json_string(
1171 json.c_str() + rule_pos, "explanation");
1172
1173 pos = rule_pos + 1;
1174 return v;
1175}
1176
1184void ConstitutionalValidator::extract_revised_field(
1185 const std::string& json, CritiqueResult& result) {
1186 result.revised = extract_json_string(json.c_str(), "revised");
1187}
1188
1189} // namespace entropic
bool auto_retry_enabled() const
Whether auto-revision is currently enabled.
void set_global_enabled(bool enabled)
Toggle the global validation gate at runtime.
entropic_error_t attach(HookInterface *hook_iface, InferenceInterface *inference)
Register this validator as a POST_GENERATE hook.
static int hook_callback(entropic_hook_point_t hook_point, const char *context_json, char **modified_json, void *user_data)
POST_GENERATE hook callback for constitutional validation.
void set_tier_rules(const std::string &identity_name, const std::vector< std::string > &rules)
Set per-identity validation rules from frontmatter.
entropic_error_t accept_last()
Finalize the cached attempt as the validation result.
entropic_error_t resume_retry()
Resume the revision pass after a paused validation.
ConstitutionalValidator(const ConstitutionalValidationConfig &config, const std::string &constitution_text)
Construct validator with config and constitution text.
bool should_validate(const std::string &identity_name) const
Check if validation is enabled for a given identity.
void set_critique_callbacks(void(*start_cb)(void *user_data), void(*end_cb)(void *user_data), void *user_data)
Register the critique start/end callback pair (gh#50).
static CritiqueResult parse_critique(const std::string &json_str)
Parse critique JSON into structured result (exposed for testing).
ValidationResult validate(const std::string &content, const std::string &tier, const char *messages_json)
Run the validation pipeline on generated content.
ValidationResult last_result() const
Get the last validation result.
void set_attempt_boundary_cb(void(*cb)(int attempt_n, void *user_data), void *user_data)
Register the attempt-boundary callback.
void set_auto_retry(bool enabled)
Enable or disable automatic revision after rejection.
std::string build_critique_prompt(const std::string &content) const
Build the critique prompt (exposed for testing).
void detach(HookInterface *hook_iface)
Deregister the POST_GENERATE hook.
void set_identity_validation(const std::string &identity_name, bool enabled)
Set per-identity validation override.
Thread-safe hook registration and dispatch.
entropic_error_t register_hook(entropic_hook_point_t point, entropic_hook_callback_t callback, void *user_data, int priority)
Register a hook callback at a hook point.
entropic_error_t deregister_hook(entropic_hook_point_t point, entropic_hook_callback_t callback, void *user_data)
Deregister a hook callback.
Post-generation constitutional compliance validator.
entropic_error_t
Error codes returned by all C API functions.
Definition error.h:35
@ ENTROPIC_OK
Success.
Definition error.h:36
@ ENTROPIC_ERROR_INVALID_ARGUMENT
NULL pointer, empty string, out-of-range value.
Definition error.h:37
@ ENTROPIC_ERROR_INVALID_STATE
Operation not valid in current state (e.g., generate before activate)
Definition error.h:39
Thread-safe hook registration and dispatch.
entropic_hook_point_t
Hook points in the engine lifecycle.
Definition hooks.h:34
@ ENTROPIC_HOOK_POST_GENERATE
1: After inference generate returns
Definition hooks.h:37
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).
@ passed_consumer_override
gh#30 (v2.1.5): consumer called accept_last() to override a paused rejection.
@ rejected_reverted_length
Revision gutted content >50%; original preserved.
@ passed
No violations, content unchanged.
@ revised
Violations found; revision applied.
@ paused_pending_consumer
gh#30 (v2.1.5): auto_retry disabled and a critique failed.
@ skipped
Validation did not run (skip_tiers / pure-tool-call / empty)
@ rejected_max_revisions
Revisions exhausted; last output returned as-is.
static bool is_pure_tool_call(const std::string &content)
Check if content is a pure tool call with no prose.
static std::string json_escape(const std::string &input)
Save pre-compaction snapshot via storage interface.
static std::string strip_think_blocks(const std::string &content)
Strip <think>...</think> blocks from content.
Constitutional validation pipeline configuration.
Definition config.h:565
int max_revisions
Max re-generation attempts (0 = critique only)
Definition config.h:567
int priority
Hook priority (higher = later)
Definition config.h:571
bool enable_thinking
Enable think-blocks for critique (default OFF)
Definition config.h:570
float temperature
Critique generation temperature.
Definition config.h:569
std::string critique_tier
Tier to route critique generation on.
Definition config.h:579
int max_critique_tokens
Token budget for critique generation.
Definition config.h:568
std::string grammar_key
Grammar registry key.
Definition config.h:572
std::vector< std::string > skip_tiers
Tiers exempt from validation (default: lead — streams before hook fires)
Definition config.h:574
Structured result from a single critique generation pass.
Definition validation.h:44
std::vector< Violation > violations
List of constitutional violations.
Definition validation.h:46
std::string raw_json
Raw critique JSON for audit logging.
Definition validation.h:48
bool compliant
true if output passes all rules
Definition validation.h:45
Context passed through hook user_data.
class ConstitutionalValidator * validator
Validator instance.
InferenceInterface * inference
For critique generation.
int attempt_n
gh#30 (v2.1.5): attempt index this result corresponds to.
Definition validation.h:95
ValidationVerdict verdict
Structured outcome (2.0.6-rc17)
Definition validation.h:90
CritiqueResult final_critique
Last critique result.
Definition validation.h:89
std::string content
Final output (original or revised)
Definition validation.h:86
int revision_count
Number of revision attempts made.
Definition validation.h:88