Entropic 2.11.1
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 bool is_pure_tool_call(const std::string& content);
28static std::string strip_reasoning(const std::string& content,
29 const std::string& open,
30 const std::string& close);
31
40 const std::string& constitution_text)
41 : config_(config),
42 constitution_text_(constitution_text),
43 global_enabled_(config.enabled) {
44 context_.validator = this;
45 context_.inference = nullptr;
46}
47
57 HookInterface* hook_iface,
58 InferenceInterface* inference) {
59 inference_ = inference;
60 context_.inference = inference;
61
62 if (hook_iface == nullptr || hook_iface->registry == nullptr) {
64 }
65
66 auto* reg = static_cast<HookRegistry*>(hook_iface->registry);
67 return reg->register_hook(
70 &context_,
71 config_.priority);
72}
73
80void ConstitutionalValidator::detach(HookInterface* hook_iface) {
81 if (hook_iface == nullptr || hook_iface->registry == nullptr) {
82 return;
83 }
84 auto* reg = static_cast<HookRegistry*>(hook_iface->registry);
85 reg->deregister_hook(
87}
88
102 const std::string& identity_name) const {
103 std::lock_guard<std::mutex> lock(overrides_mutex_);
104 auto it = identity_overrides_.find(identity_name);
105 if (it != identity_overrides_.end()) {
106 return it->second;
107 }
108 // Default skip for tiers that stream before the hook fires
109 for (const auto& skip : config_.skip_tiers) {
110 if (skip == identity_name) { return false; }
111 }
112 return global_enabled_;
113}
114
122 std::lock_guard<std::mutex> lock(overrides_mutex_);
123 global_enabled_ = enabled;
124}
125
133 std::function<std::pair<std::string, std::string>(const std::string&)>
134 resolver) {
135 std::lock_guard<std::mutex> lock(marker_mutex_);
136 marker_resolver_ = std::move(resolver);
137}
138
139// ── gh#30 (v2.1.5): consumer-driven retry controls ───────
140
148 auto_retry_enabled_.store(enabled);
149}
150
158 return auto_retry_enabled_.load();
159}
160
173 std::optional<PendingValidationState> state;
174 {
175 std::lock_guard lock(pending_mutex_);
176 state = std::move(pending_state_);
177 pending_state_.reset();
178 }
179 if (!state) {
181 }
182 auto result = apply_revisions(
183 state->result, state->critique,
184 state->messages_json.empty() ? nullptr
185 : state->messages_json.c_str());
186 store_result(result);
187 logger->info("Constitutional validation resumed (gh#30): "
188 "final verdict={}", static_cast<int>(result.verdict));
189 return ENTROPIC_OK;
190}
191
203 std::optional<PendingValidationState> state;
204 {
205 std::lock_guard lock(pending_mutex_);
206 state = std::move(pending_state_);
207 pending_state_.reset();
208 }
209 if (!state) {
211 }
212 ValidationResult result = std::move(state->result);
214 store_result(result);
215 logger->info("Constitutional validation accepted by consumer "
216 "(gh#30): attempt_n={}", result.attempt_n);
217 return ENTROPIC_OK;
218}
219
228 void (*cb)(int, void*), void* user_data) {
229 std::lock_guard<std::mutex> lock(attempt_boundary_mutex_);
230 attempt_boundary_.cb = cb;
231 attempt_boundary_.user_data = user_data;
232}
233
249 void (*start_cb)(void*),
250 void (*end_cb)(void*),
251 void* user_data) {
252 std::lock_guard<std::mutex> lock(critique_cbs_mutex_);
253 critique_cbs_.start_cb = start_cb;
254 critique_cbs_.end_cb = end_cb;
255 critique_cbs_.user_data = user_data;
256}
257
266 const std::string& identity_name, bool enabled) {
267 std::lock_guard<std::mutex> lock(overrides_mutex_);
268 identity_overrides_[identity_name] = enabled;
269}
270
279 const std::string& identity_name,
280 const std::vector<std::string>& rules) {
281 std::lock_guard<std::mutex> lock(overrides_mutex_);
282 tier_rules_[identity_name] = rules;
283}
284
302 const std::string& content,
303 const std::string& tier,
304 const char* messages_json) {
305 ValidationResult result;
306 result.content = content;
307
308 if (!should_validate(tier)) {
309 logger->info("Validation skipped for tier '{}'", tier);
311 store_result(result);
312 return result;
313 }
314
315 // gh#108 (v2.10.3): resolve this TIER's delimiters — one validator serves
316 // every tier, and thinking format is per-family.
317 std::string open = "<think>";
318 std::string close = "</think>";
319 {
320 std::lock_guard<std::mutex> lock(marker_mutex_);
321 if (marker_resolver_) { std::tie(open, close) = marker_resolver_(tier); }
322 }
323 auto cleaned = strip_reasoning(content, open, close);
324 if (cleaned.empty() || is_pure_tool_call(cleaned)) {
325 logger->info("Validation skipped: pure tool-call or empty");
327 store_result(result);
328 return result;
329 }
330
331 current_tier_ = tier;
332 logger->info("Validation start: {} chars, tier='{}'",
333 cleaned.size(), tier);
334 result = run_validation_loop(cleaned, tier, messages_json);
335 log_verdict(result);
336 store_result(result);
337 return result;
338}
339
346void ConstitutionalValidator::log_verdict(
347 const ValidationResult& result) const {
348 switch (result.verdict) {
350 logger->info("Validation passed (no violations)");
351 break;
353 logger->info("Validation revised ({} revision(s) applied)",
354 result.revision_count);
355 break;
357 logger->warn("Validation reverted "
358 "({} violation(s) found; revision discarded for length)",
359 result.final_critique.violations.size());
360 break;
362 logger->warn("Validation rejected "
363 "(max revisions exhausted; {} violation(s) remain)",
364 result.final_critique.violations.size());
365 break;
367 // log-site above already emits "Validation skipped…"
368 break;
370 logger->info("Validation paused (gh#30): "
371 "{} violation(s); awaiting consumer",
372 result.final_critique.violations.size());
373 break;
375 logger->info("Validation accepted by consumer (gh#30): "
376 "attempt_n={}", result.attempt_n);
377 break;
378 }
379}
380
388 std::lock_guard<std::mutex> lock(result_mutex_);
389 return last_result_;
390}
391
392// ── Hook Callback ────────────────────────────────────────
393
405 entropic_hook_point_t /*hook_point*/,
406 const char* context_json,
407 char** modified_json,
408 void* user_data) {
409 *modified_json = nullptr;
410 auto* ctx = static_cast<ValidationContext*>(user_data);
411 if (ctx == nullptr || ctx->validator == nullptr) {
412 return 0;
413 }
414
415 return ctx->validator->handle_hook(context_json, modified_json);
416}
417
418// ── Critique Prompt Assembly ──────────────────────────────
419
442 const std::string& content) const {
443 std::string prompt;
444 prompt.reserve(constitution_text_.size() + content.size() + 512);
445
446 prompt += "You are a compliance evaluator. "
447 "Respond ONLY with the structured JSON evaluation.\n\n";
448
449 // When per-tier rules exist: they are the primary rubric,
450 // constitution is background context. When absent: constitution
451 // is the sole rubric.
452 bool has_tier_rules = false;
453 {
454 std::lock_guard<std::mutex> lock(overrides_mutex_);
455 auto it = tier_rules_.find(current_tier_);
456 has_tier_rules = (it != tier_rules_.end()
457 && !it->second.empty());
458 if (has_tier_rules) {
459 prompt += "Evaluate against these rules for the '"
460 + current_tier_ + "' identity:\n";
461 for (const auto& rule : it->second) {
462 prompt += "- " + rule + "\n";
463 }
464 prompt += "\nBackground constitutional guidance:\n";
465 prompt += constitution_text_;
466 } else {
467 prompt += "Constitutional Rules:\n";
468 prompt += constitution_text_;
469 }
470 }
471
472 // Provide tool call manifest so validator can assess grounding
473 if (!current_tool_context_.empty()) {
474 prompt += "\n\nTool calls made this turn:\n";
475 prompt += current_tool_context_;
476 }
477
478 // Issue #5 (v2.1.3): un-pruned tool-result content. Lets the
479 // critique pass verify file:line citations against actual
480 // evidence rather than the manifest-plus-stubs that pre-2.1.3
481 // produced. Engine surfaces this when the messages have been
482 // partially pruned (#6 limits when this happens, but legitimate
483 // long-context delegations still trigger it).
484 if (!current_tool_evidence_.empty()) {
485 prompt += "\n\nTool result evidence (verify citations against this):\n";
486 prompt += current_tool_evidence_;
487 }
488
489 prompt += "\n\nEvaluate this output for compliance:\n\n---\n";
490 prompt += content;
491 prompt += "\n---";
492 return prompt;
493}
494
504 const std::string& json_str) {
505 CritiqueResult result;
506 result.raw_json = json_str;
507
508 if (!extract_compliant_field(json_str, result)) {
509 // Fail-open: if the model produced JSON we can't parse (e.g.,
510 // "constitutional_compliance_status" instead of "compliant"),
511 // treat it as compliant rather than triggering a revision loop
512 // on a parse error. The grammar constraint is the real fix;
513 // this is the safety net when grammar loading fails.
514 result.compliant = true;
515 return result;
516 }
517
518 extract_violations(json_str, result);
519 extract_revised_field(json_str, result);
520 return result;
521}
522
523// ── Private Implementation ────────────────────────────────
524
531void ConstitutionalValidator::store_result(
532 const ValidationResult& result) {
533 std::lock_guard<std::mutex> lock(result_mutex_);
534 last_result_ = result;
535}
536
547ValidationResult ConstitutionalValidator::run_validation_loop(
548 const std::string& content,
549 const std::string& tier,
550 const char* messages_json) {
551 ValidationResult result;
552 result.content = content;
553
554 auto critique = run_critique(content);
555 result.final_critique = critique;
556
557 if (critique.compliant) {
558 return result;
559 }
560
561 // gh#30 (v2.1.5): when the consumer has disabled auto-retry, stop
562 // here and stash enough state for resume_retry()/accept_last() to
563 // continue.
564 if (!auto_retry_enabled_.load()) {
566 result.attempt_n = 0;
567 {
568 std::lock_guard lock(pending_mutex_);
569 pending_state_ = PendingValidationState{
570 result, critique,
571 messages_json ? std::string(messages_json) : std::string{},
572 tier};
573 }
574 logger->info("Constitutional validation paused (gh#30): "
575 "auto_retry disabled, awaiting consumer decision");
576 return result;
577 }
578
579 return apply_revisions(result, critique, messages_json);
580}
581
600ValidationResult ConstitutionalValidator::apply_revisions(
601 ValidationResult result,
602 const CritiqueResult& initial_critique,
603 const char* messages_json) {
604 auto critique = initial_critique;
605 for (int i = 0; i < config_.max_revisions; ++i) {
606 const auto& before = result.content;
607 // gh#30 (v2.1.5): fire attempt-boundary callback before the
608 // revision so consumers can split rendered output cleanly.
609 // Snapshot under the mutex first so a concurrent
610 // set_attempt_boundary_cb() cannot tear the {cb, user_data}
611 // pair mid-call (post-2.1.5 verification hardening).
612 AttemptBoundaryCb cb_snap;
613 {
614 std::lock_guard<std::mutex> lk(attempt_boundary_mutex_);
615 cb_snap = attempt_boundary_;
616 }
617 if (cb_snap.cb != nullptr) {
618 try {
619 cb_snap.cb(i + 1, cb_snap.user_data);
620 } catch (...) {
621 logger->warn("attempt_boundary_cb threw; swallowed at "
622 ".so boundary (gh#30)");
623 }
624 }
625 auto revised = attempt_revision(before, critique, messages_json);
626
627 // Length safety valve: reject revisions that gut the content
628 if (revised.size() < before.size() / 2) {
629 logger->warn("Constitutional validation: revision {}/{} "
630 "shrank content {}→{} chars (>50%); "
631 "discarding revision, returning original",
632 i + 1, config_.max_revisions,
633 before.size(), revised.size());
634 result.verdict =
636 result.attempt_n = i + 1;
637 return result;
638 }
639
640 result.content = revised;
641 result.was_revised = true;
642 result.revision_count = i + 1;
643 result.attempt_n = i + 1;
644
645 critique = run_critique(revised);
646 result.final_critique = critique;
647
648 if (critique.compliant) { break; }
649 }
650
651 if (!critique.compliant) {
652 logger->warn("Constitutional validation: max revisions ({}) "
653 "exhausted, returning last output",
654 config_.max_revisions);
656 } else if (result.was_revised) {
657 result.verdict = ValidationVerdict::revised;
658 }
659 return result;
660}
661
671std::string ConstitutionalValidator::attempt_revision(
672 const std::string& content,
673 const CritiqueResult& critique,
674 const char* messages_json) {
675 if (!critique.revised.empty()) {
676 return critique.revised;
677 }
678 return revise(content, critique, messages_json);
679}
680
689CritiqueResult ConstitutionalValidator::run_critique(
690 const std::string& content) {
691 if (inference_ == nullptr || inference_->generate == nullptr) {
692 logger->warn("Constitutional validation: no inference "
693 "interface, skipping critique");
694 return {};
695 }
696
697 auto messages = build_critique_messages(content);
698 auto params = build_critique_params();
699 char* result_json = nullptr;
700
701 // gh#50 (v2.1.12): snapshot the callback pair under the mutex
702 // so a consumer reassigning the slot mid-critique cannot tear
703 // {start_cb, end_cb, user_data}. Invoke OUTSIDE the lock so a
704 // pathological consumer-side handler that re-enters
705 // set_critique_callbacks doesn't self-deadlock.
706 CritiqueCallbacks cbs;
707 {
708 std::lock_guard<std::mutex> lock(critique_cbs_mutex_);
709 cbs = critique_cbs_;
710 }
711 if (cbs.start_cb != nullptr) { cbs.start_cb(cbs.user_data); }
712
713 int rc = inference_->generate(
714 messages.c_str(), params.c_str(),
715 &result_json, inference_->backend_data);
716
717 if (cbs.end_cb != nullptr) { cbs.end_cb(cbs.user_data); }
718
719 if (rc != 0 || result_json == nullptr) {
720 logger->warn("Constitutional validation: critique generation "
721 "failed (rc={})", rc);
722 return {};
723 }
724
725 std::string raw(result_json);
726 if (inference_->free_fn != nullptr) {
727 inference_->free_fn(result_json);
728 }
729
730 return parse_critique(raw);
731}
732
740std::string ConstitutionalValidator::build_critique_messages(
741 const std::string& content) const {
742 auto prompt = build_critique_prompt(content);
743 return build_single_turn_json(prompt);
744}
745
752std::string ConstitutionalValidator::build_critique_params() const {
753 std::string params = "{\"grammar_key\":\"";
754 params += config_.grammar_key;
755 params += "\",\"max_tokens\":";
756 params += std::to_string(config_.max_critique_tokens);
757 params += ",\"temperature\":";
758 params += std::to_string(config_.temperature);
759 params += ",\"enable_thinking\":";
760 params += config_.enable_thinking ? "true" : "false";
761 // E4 (2.0.6-rc17): route critique on a dedicated (typically
762 // smaller) tier so grammar-constrained sampling doesn't burn
763 // 35B primary inference. Empty -> default_tier selected by
764 // the inference interface.
765 if (!config_.critique_tier.empty()) {
766 params += ",\"tier\":\"";
767 params += config_.critique_tier;
768 params += "\"";
769 }
770 params += "}";
771 return params;
772}
773
790std::string ConstitutionalValidator::revise(
791 const std::string& original,
792 const CritiqueResult& critique,
793 const char* messages_json) {
794 if (inference_ == nullptr || inference_->generate == nullptr) {
795 return original;
796 }
797
798 auto augmented = build_revision_messages(
799 original, critique, messages_json);
800 char* result_json = nullptr;
801
802 // Unconstrained generation: revision output is free-form prose,
803 // not the structured JSON schema used for the critique pass.
804 int rc = inference_->generate(
805 augmented.c_str(), "{}", &result_json,
806 inference_->backend_data);
807
808 if (rc != 0 || result_json == nullptr) {
809 logger->warn("Constitutional validation: revision generation "
810 "failed (rc={})", rc);
811 return original;
812 }
813
814 std::string revised(result_json);
815 if (inference_->free_fn != nullptr) {
816 inference_->free_fn(result_json);
817 }
818 return revised;
819}
820
821// ── JSON String Helpers ──────────────────────────────────
822// Manual JSON construction avoids nlohmann/json dependency in core.so.
823
831static std::string json_escape(const std::string& s) {
832 std::string out;
833 out.reserve(s.size() + 16);
834 for (char c : s) {
835 switch (c) {
836 case '"': out += "\\\""; break;
837 case '\\': out += "\\\\"; break;
838 case '\n': out += "\\n"; break;
839 case '\r': out += "\\r"; break;
840 case '\t': out += "\\t"; break;
841 default: out += c; break;
842 }
843 }
844 return out;
845}
846
854std::string ConstitutionalValidator::build_single_turn_json(
855 const std::string& prompt) const {
856 std::string json = "[{\"role\":\"user\",\"content\":\"";
857 json += json_escape(prompt);
858 json += "\"}]";
859 return json;
860}
861
871std::string ConstitutionalValidator::build_revision_messages(
872 const std::string& original,
873 const CritiqueResult& critique,
874 const char* messages_json) const {
875 std::string feedback = build_feedback_text(critique);
876 return inject_feedback_into_messages(
877 original, feedback, messages_json);
878}
879
887std::string ConstitutionalValidator::build_feedback_text(
888 const CritiqueResult& critique) const {
889 std::string feedback =
890 "Your response violated these constitutional rules:\\n";
891 for (const auto& v : critique.violations) {
892 feedback += "- " + v.rule + ": " + v.explanation + "\\n";
893 }
894 feedback += "Please revise your response to comply with all "
895 "constitutional rules.";
896 return feedback;
897}
898
914std::string ConstitutionalValidator::inject_feedback_into_messages(
915 const std::string& original,
916 const std::string& feedback,
917 const char* messages_json) const {
918 std::string base;
919 if (messages_json != nullptr) {
920 base = std::string(messages_json);
921 } else {
922 // Prefer identity system prompt over bare constitution — keeps
923 // the model in persona during revision (prevents apology spirals).
924 const auto& sys = !current_system_prompt_.empty()
925 ? current_system_prompt_ : constitution_text_;
926 base = "[{\"role\":\"system\",\"content\":\""
927 + json_escape(sys) + "\"}";
928 }
929
930 // Strip trailing ]
931 if (!base.empty() && base.back() == ']') {
932 base.pop_back();
933 }
934
935 // Add comma if there were existing messages
936 if (base.size() > 1) {
937 base += ",";
938 }
939
940 base += "{\"role\":\"assistant\",\"content\":\"";
941 base += json_escape(original);
942 base += "\"},{\"role\":\"system\",\"content\":\"";
943 base += json_escape("[CONSTITUTIONAL REVIEW] " + feedback);
944 base += "\"}]";
945 return base;
946}
947
972static std::string strip_reasoning(const std::string& content,
973 const std::string& open,
974 const std::string& close) {
975 std::string result;
976 size_t pos = 0;
977 while (pos < content.size()) {
978 auto o = content.find(open, pos);
979 if (o == std::string::npos) {
980 result.append(content, pos);
981 break;
982 }
983 result.append(content, pos, o - pos);
984 auto c = content.find(close, o + open.size());
985 pos = (c == std::string::npos) ? content.size() : c + close.size();
986 }
987 return result;
988}
989
1002static bool is_pure_tool_call(const std::string& content) {
1003 std::string stripped = content;
1004 // Remove all <tool_call>...</tool_call> blocks
1005 while (true) {
1006 auto open = stripped.find("<tool_call>");
1007 if (open == std::string::npos) { break; }
1008 auto close = stripped.find("</tool_call>", open);
1009 if (close == std::string::npos) { break; }
1010 stripped.erase(open, close + 12 - open);
1011 }
1012 // If only whitespace remains, it was a pure tool call
1013 return stripped.find_first_not_of(" \t\n\r") == std::string::npos;
1014}
1015
1031int ConstitutionalValidator::handle_hook(
1032 const char* context_json,
1033 char** modified_json) {
1034 *modified_json = nullptr;
1035
1036 auto content = extract_json_string(context_json, "content");
1037 auto tier = extract_json_string(context_json, "tier");
1038
1039 if (content.empty()) { return 0; }
1040
1041 // Store per-call context for build_critique_prompt and revision
1042 current_tool_context_ = extract_json_string(
1043 context_json, "tool_context");
1044 // Issue #5 (v2.1.3): un-pruned tool-result content surfaced by the
1045 // engine so the validator can verify citations against actual
1046 // evidence rather than post-prune stubs. Optional field — pre-2.1.3
1047 // engines that don't send it give an empty string, falling back to
1048 // the manifest-only behaviour.
1049 current_tool_evidence_ = extract_json_string(
1050 context_json, "tool_evidence");
1051 current_system_prompt_ = extract_json_string(
1052 context_json, "system_prompt");
1053
1054 auto result = validate(content, tier, nullptr);
1055 if (!result.was_revised) {
1056 return 0;
1057 }
1058
1059 write_modified_json(result.content, modified_json);
1060 return 0;
1061}
1062
1070void ConstitutionalValidator::write_modified_json(
1071 const std::string& content,
1072 char** modified_json) {
1073 std::string out = "{\"content\":\"";
1074 out += json_escape(content);
1075 out += "\"}";
1076
1077 auto* buf = static_cast<char*>(malloc(out.size() + 1));
1078 if (buf != nullptr) {
1079 std::memcpy(buf, out.c_str(), out.size() + 1);
1080 *modified_json = buf;
1081 }
1082}
1083
1084// ── Minimal JSON Field Extraction ─────────────────────────
1085// No nlohmann/json in core.so. These extract known fields from
1086// simple flat JSON objects by string search.
1087
1096std::string ConstitutionalValidator::extract_json_string(
1097 const char* json, const char* key) {
1098 if (json == nullptr || key == nullptr) {
1099 return {};
1100 }
1101
1102 std::string needle = std::string("\"") + key + "\"";
1103 const char* pos = strstr(json, needle.c_str());
1104 if (pos == nullptr) {
1105 return {};
1106 }
1107
1108 return extract_string_after_colon(pos + needle.size());
1109}
1110
1118std::string ConstitutionalValidator::extract_string_after_colon(
1119 const char* pos) {
1120 // Skip whitespace and colon
1121 while (*pos == ' ' || *pos == ':' || *pos == '\t') {
1122 ++pos;
1123 }
1124 if (*pos != '"') {
1125 return {};
1126 }
1127 ++pos; // skip opening quote
1128
1129 std::string value;
1130 while (*pos != '\0' && *pos != '"') {
1131 if (*pos == '\\' && *(pos + 1) != '\0') {
1132 ++pos;
1133 switch (*pos) {
1134 case 'n': value += '\n'; break;
1135 case 't': value += '\t'; break;
1136 case 'r': value += '\r'; break;
1137 default: value += *pos; break;
1138 }
1139 } else {
1140 value += *pos;
1141 }
1142 ++pos;
1143 }
1144 return value;
1145}
1146
1155bool ConstitutionalValidator::extract_compliant_field(
1156 const std::string& json, CritiqueResult& result) {
1157 auto pos = json.find("\"compliant\"");
1158 if (pos == std::string::npos) {
1159 logger->warn("Constitutional validation: malformed critique "
1160 "JSON — missing 'compliant' field");
1161 return false;
1162 }
1163
1164 result.compliant = (json.find("true", pos) < json.find("false", pos));
1165 return true;
1166}
1167
1175void ConstitutionalValidator::extract_violations(
1176 const std::string& json, CritiqueResult& result) {
1177 size_t search_pos = 0;
1178
1179 while (true) {
1180 auto v = extract_next_violation(json, search_pos);
1181 if (!v.has_value()) {
1182 break;
1183 }
1184 result.violations.push_back(std::move(v.value()));
1185 }
1186}
1187
1196std::optional<Violation>
1197ConstitutionalValidator::extract_next_violation(
1198 const std::string& json, size_t& pos) {
1199 auto rule_pos = json.find("\"rule\"", pos);
1200 if (rule_pos == std::string::npos) {
1201 return std::nullopt;
1202 }
1203
1204 Violation v;
1205 v.rule = extract_json_string(
1206 json.c_str() + rule_pos, "rule");
1207 v.excerpt = extract_json_string(
1208 json.c_str() + rule_pos, "excerpt");
1209 v.explanation = extract_json_string(
1210 json.c_str() + rule_pos, "explanation");
1211
1212 pos = rule_pos + 1;
1213 return v;
1214}
1215
1223void ConstitutionalValidator::extract_revised_field(
1224 const std::string& json, CritiqueResult& result) {
1225 result.revised = extract_json_string(json.c_str(), "revised");
1226}
1227
1228} // 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_marker_resolver(std::function< std::pair< std::string, std::string >(const std::string &)> resolver)
Register a per-tier reasoning-delimiter resolver (gh#108).
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:37
@ ENTROPIC_OK
Success.
Definition error.h:38
@ ENTROPIC_ERROR_INVALID_ARGUMENT
NULL pointer, empty string, out-of-range value.
Definition error.h:39
@ ENTROPIC_ERROR_INVALID_STATE
Operation not valid in current state (e.g., generate before activate)
Definition error.h:41
Thread-safe hook registration and dispatch.
entropic_hook_point_t
Hook points in the engine lifecycle.
Definition hooks.h:39
@ ENTROPIC_HOOK_POST_GENERATE
1: After inference generate returns
Definition hooks.h:42
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::string strip_reasoning(const std::string &content, const std::string &open, const std::string &close)
Strip reasoning blocks from content before critique (gh#108).
@ 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.
Constitutional validation pipeline configuration.
Definition config.h:824
int max_revisions
Max re-generation attempts (0 = critique only)
Definition config.h:826
int priority
Hook priority (higher = later)
Definition config.h:830
bool enable_thinking
Enable think-blocks for critique (default OFF)
Definition config.h:829
float temperature
Critique generation temperature.
Definition config.h:828
std::string critique_tier
Tier to route critique generation on.
Definition config.h:838
int max_critique_tokens
Token budget for critique generation.
Definition config.h:827
std::string grammar_key
Grammar registry key.
Definition config.h:831
std::vector< std::string > skip_tiers
Tiers exempt from validation (default: lead — streams before hook fires)
Definition config.h:833
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