Entropic 2.14.3
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 {
23
29thread_local std::string t_current_tier;
30thread_local std::string t_current_tool_context;
31thread_local std::string t_current_tool_evidence;
32thread_local std::string t_current_system_prompt;
33
34} // namespace
35
42const std::string& ConstitutionalValidator::current_tier() {
43 return t_current_tier;
44}
45
52const std::string& ConstitutionalValidator::current_tool_context() {
54}
55
62const std::string& ConstitutionalValidator::current_tool_evidence() {
64}
65
72const std::string& ConstitutionalValidator::current_system_prompt() {
74}
75
85void ConstitutionalValidator::set_turn_context(
86 const std::string& tier,
87 const std::string& tool_context,
88 const std::string& tool_evidence,
89 const std::string& system_prompt) {
93 t_current_system_prompt = system_prompt;
94}
95
96namespace {
97auto logger = entropic::log::get("core.constitutional_validator");
98} // anonymous namespace
99
100// Forward declarations for static helpers used in validate()
101static bool is_pure_tool_call(const std::string& content);
102static std::string strip_reasoning(const std::string& content,
103 const std::string& open,
104 const std::string& close);
105
113 const ConstitutionalValidationConfig& config,
114 const std::string& constitution_text)
115 : config_(config),
116 constitution_text_(constitution_text),
117 global_enabled_(config.enabled) {
118 context_.validator = this;
119 context_.inference = nullptr;
120}
121
132 InferenceInterface* inference) {
133 inference_ = inference;
134 context_.inference = inference;
135
136 if (hook_iface == nullptr || hook_iface->registry == nullptr) {
138 }
139
140 auto* reg = static_cast<HookRegistry*>(hook_iface->registry);
141 return reg->register_hook(
144 &context_,
145 config_.priority);
146}
147
155 if (hook_iface == nullptr || hook_iface->registry == nullptr) {
156 return;
157 }
158 auto* reg = static_cast<HookRegistry*>(hook_iface->registry);
161}
162
176 const std::string& identity_name) const {
177 std::lock_guard<std::mutex> lock(overrides_mutex_);
178 auto it = identity_overrides_.find(identity_name);
179 if (it != identity_overrides_.end()) {
180 return it->second;
181 }
182 // Default skip for tiers that stream before the hook fires
183 for (const auto& skip : config_.skip_tiers) {
184 if (skip == identity_name) { return false; }
185 }
186 return global_enabled_;
187}
188
196 std::lock_guard<std::mutex> lock(overrides_mutex_);
197 global_enabled_ = enabled;
198}
199
207 std::function<std::pair<std::string, std::string>(const std::string&)>
208 resolver) {
209 std::lock_guard<std::mutex> lock(marker_mutex_);
210 marker_resolver_ = std::move(resolver);
211}
212
213// ── gh#30 (v2.1.5): consumer-driven retry controls ───────
214
222 auto_retry_enabled_.store(enabled);
223}
224
232 return auto_retry_enabled_.load();
233}
234
247 std::optional<PendingValidationState> state;
248 {
249 std::lock_guard lock(pending_mutex_);
250 state = std::move(pending_state_);
251 pending_state_.reset();
252 }
253 if (!state) {
255 }
256 // gh#158: re-publish the paused turn's identity prompt onto THIS thread.
257 // The revision fallback in inject_feedback_into_messages reads it, and
258 // the consumer resumes from a thread that never ran that turn.
259 set_turn_context(state->tier, "", "", state->system_prompt);
260 auto result = apply_revisions(
261 state->result, state->critique,
262 state->messages_json.empty() ? nullptr
263 : state->messages_json.c_str());
264 store_result(result);
265 logger->info("Constitutional validation resumed (gh#30): "
266 "final verdict={}", static_cast<int>(result.verdict));
267 return ENTROPIC_OK;
268}
269
281 std::optional<PendingValidationState> state;
282 {
283 std::lock_guard lock(pending_mutex_);
284 state = std::move(pending_state_);
285 pending_state_.reset();
286 }
287 if (!state) {
289 }
290 ValidationResult result = std::move(state->result);
292 store_result(result);
293 logger->info("Constitutional validation accepted by consumer "
294 "(gh#30): attempt_n={}", result.attempt_n);
295 return ENTROPIC_OK;
296}
297
306 void (*cb)(int, void*), void* user_data) {
307 std::lock_guard<std::mutex> lock(attempt_boundary_mutex_);
308 attempt_boundary_.cb = cb;
309 attempt_boundary_.user_data = user_data;
310}
311
327 void (*start_cb)(void*),
328 void (*end_cb)(void*),
329 void* user_data) {
330 std::lock_guard<std::mutex> lock(critique_cbs_mutex_);
331 critique_cbs_.start_cb = start_cb;
332 critique_cbs_.end_cb = end_cb;
333 critique_cbs_.user_data = user_data;
334}
335
344 const std::string& identity_name, bool enabled) {
345 std::lock_guard<std::mutex> lock(overrides_mutex_);
346 identity_overrides_[identity_name] = enabled;
347}
348
357 const std::string& identity_name,
358 const std::vector<std::string>& rules) {
359 std::lock_guard<std::mutex> lock(overrides_mutex_);
360 tier_rules_[identity_name] = rules;
361}
362
380 const std::string& content,
381 const std::string& tier,
382 const char* messages_json) {
383 ValidationResult result;
384 result.content = content;
385
386 if (!should_validate(tier)) {
387 logger->info("Validation skipped for tier '{}'", tier);
389 store_result(result);
390 return result;
391 }
392
393 // gh#108 (v2.10.3): resolve this TIER's delimiters — one validator serves
394 // every tier, and thinking format is per-family.
395 std::string open = "<think>";
396 std::string close = "</think>";
397 {
398 std::lock_guard<std::mutex> lock(marker_mutex_);
399 if (marker_resolver_) { std::tie(open, close) = marker_resolver_(tier); }
400 }
401 auto cleaned = strip_reasoning(content, open, close);
402 if (cleaned.empty() || is_pure_tool_call(cleaned)) {
403 logger->info("Validation skipped: pure tool-call or empty");
405 store_result(result);
406 return result;
407 }
408
410 logger->info("Validation start: {} chars, tier='{}'",
411 cleaned.size(), tier);
412 result = run_validation_loop(cleaned, tier, messages_json);
413 log_verdict(result);
414 store_result(result);
415 return result;
416}
417
424void ConstitutionalValidator::log_verdict(
425 const ValidationResult& result) const {
426 switch (result.verdict) {
428 logger->info("Validation passed (no violations)");
429 break;
431 logger->info("Validation revised ({} revision(s) applied)",
432 result.revision_count);
433 break;
435 logger->warn("Validation reverted "
436 "({} violation(s) found; revision discarded for length)",
437 result.final_critique.violations.size());
438 break;
440 logger->warn("Validation rejected "
441 "(max revisions exhausted; {} violation(s) remain)",
442 result.final_critique.violations.size());
443 break;
445 // log-site above already emits "Validation skipped…"
446 break;
448 logger->info("Validation paused (gh#30): "
449 "{} violation(s); awaiting consumer",
450 result.final_critique.violations.size());
451 break;
453 logger->info("Validation accepted by consumer (gh#30): "
454 "attempt_n={}", result.attempt_n);
455 break;
456 }
457}
458
466 std::lock_guard<std::mutex> lock(result_mutex_);
467 return last_result_;
468}
469
470// ── Hook Callback ────────────────────────────────────────
471
483 entropic_hook_point_t /*hook_point*/,
484 const char* context_json,
485 char** modified_json,
486 void* user_data) {
487 *modified_json = nullptr;
488 auto* ctx = static_cast<ValidationContext*>(user_data);
489 if (ctx == nullptr || ctx->validator == nullptr) {
490 return 0;
491 }
492
493 return ctx->validator->handle_hook(context_json, modified_json);
494}
495
496// ── Critique Prompt Assembly ──────────────────────────────
497
520 const std::string& content) const {
521 std::string prompt;
522 prompt.reserve(constitution_text_.size() + content.size() + 512);
523
524 prompt += "You are a compliance evaluator. "
525 "Respond ONLY with the structured JSON evaluation.\n\n";
526
527 // When per-tier rules exist: they are the primary rubric,
528 // constitution is background context. When absent: constitution
529 // is the sole rubric.
530 bool has_tier_rules = false;
531 {
532 std::lock_guard<std::mutex> lock(overrides_mutex_);
533 auto it = tier_rules_.find(current_tier());
534 has_tier_rules = (it != tier_rules_.end()
535 && !it->second.empty());
536 if (has_tier_rules) {
537 prompt += "Evaluate against these rules for the '"
538 + current_tier() + "' identity:\n";
539 for (const auto& rule : it->second) {
540 prompt += "- " + rule + "\n";
541 }
542 prompt += "\nBackground constitutional guidance:\n";
543 prompt += constitution_text_;
544 } else {
545 prompt += "Constitutional Rules:\n";
546 prompt += constitution_text_;
547 }
548 }
549
550 append_turn_evidence(prompt);
551
552 prompt += "\n\nEvaluate this output for compliance:\n\n---\n";
553 prompt += content;
554 prompt += "\n---";
555 return prompt;
556}
557
577 std::string& prompt) const {
578 const std::string& manifest = current_tool_context();
579 if (!manifest.empty()) {
580 prompt += "\n\nTool calls made this turn:\n";
581 prompt += manifest;
582 }
583 const std::string& evidence = current_tool_evidence();
584 if (!evidence.empty()) {
585 prompt += "\n\nTool result evidence (verify citations against this):\n";
586 prompt += evidence;
587 }
588}
589
599 const std::string& json_str) {
600 CritiqueResult result;
601 result.raw_json = json_str;
602
603 if (!extract_compliant_field(json_str, result)) {
604 // Fail-open: if the model produced JSON we can't parse (e.g.,
605 // "constitutional_compliance_status" instead of "compliant"),
606 // treat it as compliant rather than triggering a revision loop
607 // on a parse error. The grammar constraint is the real fix;
608 // this is the safety net when grammar loading fails.
609 result.compliant = true;
610 return result;
611 }
612
613 extract_violations(json_str, result);
614 extract_revised_field(json_str, result);
615 return result;
616}
617
618// ── Private Implementation ────────────────────────────────
619
626void ConstitutionalValidator::store_result(
627 const ValidationResult& result) {
628 std::lock_guard<std::mutex> lock(result_mutex_);
629 last_result_ = result;
630}
631
642ValidationResult ConstitutionalValidator::run_validation_loop(
643 const std::string& content,
644 const std::string& tier,
645 const char* messages_json) {
646 ValidationResult result;
647 result.content = content;
648
649 auto critique = run_critique(content);
650 result.final_critique = critique;
651
652 if (critique.compliant) {
653 return result;
654 }
655
656 // gh#30 (v2.1.5): when the consumer has disabled auto-retry, stop
657 // here and stash enough state for resume_retry()/accept_last() to
658 // continue.
659 if (!auto_retry_enabled_.load()) {
661 result.attempt_n = 0;
662 {
663 std::lock_guard lock(pending_mutex_);
664 pending_state_ = PendingValidationState{
665 result, critique,
666 messages_json ? std::string(messages_json) : std::string{},
668 }
669 logger->info("Constitutional validation paused (gh#30): "
670 "auto_retry disabled, awaiting consumer decision");
671 return result;
672 }
673
674 return apply_revisions(result, critique, messages_json);
675}
676
695ValidationResult ConstitutionalValidator::apply_revisions(
696 ValidationResult result,
697 const CritiqueResult& initial_critique,
698 const char* messages_json) {
699 auto critique = initial_critique;
700 for (int i = 0; i < config_.max_revisions; ++i) {
701 const auto& before = result.content;
702 // gh#30 (v2.1.5): fire attempt-boundary callback before the
703 // revision so consumers can split rendered output cleanly.
704 // Snapshot under the mutex first so a concurrent
705 // set_attempt_boundary_cb() cannot tear the {cb, user_data}
706 // pair mid-call (post-2.1.5 verification hardening).
707 AttemptBoundaryCb cb_snap;
708 {
709 std::lock_guard<std::mutex> lk(attempt_boundary_mutex_);
710 cb_snap = attempt_boundary_;
711 }
712 if (cb_snap.cb != nullptr) {
713 try {
714 cb_snap.cb(i + 1, cb_snap.user_data);
715 } catch (...) {
716 logger->warn("attempt_boundary_cb threw; swallowed at "
717 ".so boundary (gh#30)");
718 }
719 }
720 auto revised = attempt_revision(before, critique, messages_json);
721
722 // Length safety valve: reject revisions that gut the content
723 if (revised.size() < before.size() / 2) {
724 logger->warn("Constitutional validation: revision {}/{} "
725 "shrank content {}→{} chars (>50%); "
726 "discarding revision, returning original",
727 i + 1, config_.max_revisions,
728 before.size(), revised.size());
729 result.verdict =
731 result.attempt_n = i + 1;
732 return result;
733 }
734
735 result.content = revised;
736 result.was_revised = true;
737 result.revision_count = i + 1;
738 result.attempt_n = i + 1;
739
740 critique = run_critique(revised);
741 result.final_critique = critique;
742
743 if (critique.compliant) { break; }
744 }
745
746 if (!critique.compliant) {
747 logger->warn("Constitutional validation: max revisions ({}) "
748 "exhausted, returning last output",
749 config_.max_revisions);
751 } else if (result.was_revised) {
752 result.verdict = ValidationVerdict::revised;
753 }
754 return result;
755}
756
766std::string ConstitutionalValidator::attempt_revision(
767 const std::string& content,
768 const CritiqueResult& critique,
769 const char* messages_json) {
770 if (!critique.revised.empty()) {
771 return critique.revised;
772 }
773 return revise(content, critique, messages_json);
774}
775
784CritiqueResult ConstitutionalValidator::run_critique(
785 const std::string& content) {
786 if (inference_ == nullptr || inference_->generate == nullptr) {
787 logger->warn("Constitutional validation: no inference "
788 "interface, skipping critique");
789 return {};
790 }
791
792 auto messages = build_critique_messages(content);
793 auto params = build_critique_params();
794 char* result_json = nullptr;
795
796 // gh#50 (v2.1.12): snapshot the callback pair under the mutex
797 // so a consumer reassigning the slot mid-critique cannot tear
798 // {start_cb, end_cb, user_data}. Invoke OUTSIDE the lock so a
799 // pathological consumer-side handler that re-enters
800 // set_critique_callbacks doesn't self-deadlock.
801 CritiqueCallbacks cbs;
802 {
803 std::lock_guard<std::mutex> lock(critique_cbs_mutex_);
804 cbs = critique_cbs_;
805 }
806 if (cbs.start_cb != nullptr) { cbs.start_cb(cbs.user_data); }
807
808 int rc = inference_->generate(
809 messages.c_str(), params.c_str(),
810 &result_json, inference_->backend_data);
811
812 if (cbs.end_cb != nullptr) { cbs.end_cb(cbs.user_data); }
813
814 if (rc != 0 || result_json == nullptr) {
815 logger->warn("Constitutional validation: critique generation "
816 "failed (rc={})", rc);
817 return {};
818 }
819
820 std::string raw(result_json);
821 if (inference_->free_fn != nullptr) {
822 inference_->free_fn(result_json);
823 }
824
825 return parse_critique(raw);
826}
827
835std::string ConstitutionalValidator::build_critique_messages(
836 const std::string& content) const {
837 auto prompt = build_critique_prompt(content);
838 return build_single_turn_json(prompt);
839}
840
847std::string ConstitutionalValidator::build_critique_params() const {
848 std::string params = "{\"grammar_key\":\"";
849 params += config_.grammar_key;
850 params += "\",\"max_tokens\":";
851 params += std::to_string(config_.max_critique_tokens);
852 params += ",\"temperature\":";
853 params += std::to_string(config_.temperature);
854 params += ",\"enable_thinking\":";
855 params += config_.enable_thinking ? "true" : "false";
856 // E4 (2.0.6-rc17): route critique on a dedicated (typically
857 // smaller) tier so grammar-constrained sampling doesn't burn
858 // 35B primary inference. Empty -> default_tier selected by
859 // the inference interface.
860 if (!config_.critique_tier.empty()) {
861 params += ",\"tier\":\"";
862 params += config_.critique_tier;
863 params += "\"";
864 }
865 params += "}";
866 return params;
867}
868
885std::string ConstitutionalValidator::revise(
886 const std::string& original,
887 const CritiqueResult& critique,
888 const char* messages_json) {
889 if (inference_ == nullptr || inference_->generate == nullptr) {
890 return original;
891 }
892
893 auto augmented = build_revision_messages(
894 original, critique, messages_json);
895 char* result_json = nullptr;
896
897 // Unconstrained generation: revision output is free-form prose,
898 // not the structured JSON schema used for the critique pass.
899 int rc = inference_->generate(
900 augmented.c_str(), "{}", &result_json,
901 inference_->backend_data);
902
903 if (rc != 0 || result_json == nullptr) {
904 logger->warn("Constitutional validation: revision generation "
905 "failed (rc={})", rc);
906 return original;
907 }
908
909 std::string revised(result_json);
910 if (inference_->free_fn != nullptr) {
911 inference_->free_fn(result_json);
912 }
913 return revised;
914}
915
916// ── JSON String Helpers ──────────────────────────────────
917// Manual JSON construction avoids nlohmann/json dependency in core.so.
918
926static std::string json_escape(const std::string& s) {
927 std::string out;
928 out.reserve(s.size() + 16);
929 for (char c : s) {
930 switch (c) {
931 case '"': out += "\\\""; break;
932 case '\\': out += "\\\\"; break;
933 case '\n': out += "\\n"; break;
934 case '\r': out += "\\r"; break;
935 case '\t': out += "\\t"; break;
936 default: out += c; break;
937 }
938 }
939 return out;
940}
941
949std::string ConstitutionalValidator::build_single_turn_json(
950 const std::string& prompt) const {
951 std::string json = "[{\"role\":\"user\",\"content\":\"";
952 json += json_escape(prompt);
953 json += "\"}]";
954 return json;
955}
956
966std::string ConstitutionalValidator::build_revision_messages(
967 const std::string& original,
968 const CritiqueResult& critique,
969 const char* messages_json) const {
970 std::string feedback = build_feedback_text(critique);
971 return inject_feedback_into_messages(
972 original, feedback, messages_json);
973}
974
982std::string ConstitutionalValidator::build_feedback_text(
983 const CritiqueResult& critique) const {
984 std::string feedback =
985 "Your response violated these constitutional rules:\\n";
986 for (const auto& v : critique.violations) {
987 feedback += "- " + v.rule + ": " + v.explanation + "\\n";
988 }
989 feedback += "Please revise your response to comply with all "
990 "constitutional rules.";
991 return feedback;
992}
993
1009std::string ConstitutionalValidator::inject_feedback_into_messages(
1010 const std::string& original,
1011 const std::string& feedback,
1012 const char* messages_json) const {
1013 std::string base;
1014 if (messages_json != nullptr) {
1015 base = std::string(messages_json);
1016 } else {
1017 // Prefer identity system prompt over bare constitution — keeps
1018 // the model in persona during revision (prevents apology spirals).
1019 const auto& sys = !current_system_prompt().empty()
1020 ? current_system_prompt() : constitution_text_;
1021 base = "[{\"role\":\"system\",\"content\":\""
1022 + json_escape(sys) + "\"}";
1023 }
1024
1025 // Strip trailing ]
1026 if (!base.empty() && base.back() == ']') {
1027 base.pop_back();
1028 }
1029
1030 // Add comma if there were existing messages
1031 if (base.size() > 1) {
1032 base += ",";
1033 }
1034
1035 base += "{\"role\":\"assistant\",\"content\":\"";
1036 base += json_escape(original);
1037 base += "\"},{\"role\":\"system\",\"content\":\"";
1038 base += json_escape("[CONSTITUTIONAL REVIEW] " + feedback);
1039 base += "\"}]";
1040 return base;
1041}
1042
1067static std::string strip_reasoning(const std::string& content,
1068 const std::string& open,
1069 const std::string& close) {
1070 std::string result;
1071 size_t pos = 0;
1072 while (pos < content.size()) {
1073 auto o = content.find(open, pos);
1074 if (o == std::string::npos) {
1075 result.append(content, pos);
1076 break;
1077 }
1078 result.append(content, pos, o - pos);
1079 auto c = content.find(close, o + open.size());
1080 pos = (c == std::string::npos) ? content.size() : c + close.size();
1081 }
1082 return result;
1083}
1084
1097static bool is_pure_tool_call(const std::string& content) {
1098 std::string stripped = content;
1099 // Remove all <tool_call>...</tool_call> blocks
1100 while (true) {
1101 auto open = stripped.find("<tool_call>");
1102 if (open == std::string::npos) { break; }
1103 auto close = stripped.find("</tool_call>", open);
1104 if (close == std::string::npos) { break; }
1105 stripped.erase(open, close + 12 - open);
1106 }
1107 // If only whitespace remains, it was a pure tool call
1108 return stripped.find_first_not_of(" \t\n\r") == std::string::npos;
1109}
1110
1126int ConstitutionalValidator::handle_hook(
1127 const char* context_json,
1128 char** modified_json) {
1129 *modified_json = nullptr;
1130
1131 auto content = extract_json_string(context_json, "content");
1132 auto tier = extract_json_string(context_json, "tier");
1133
1134 if (content.empty()) { return 0; }
1135
1136 // Store per-call context for build_critique_prompt and revision
1137 const auto tool_context = extract_json_string(
1138 context_json, "tool_context");
1139 // Issue #5 (v2.1.3): un-pruned tool-result content surfaced by the
1140 // engine so the validator can verify citations against actual
1141 // evidence rather than post-prune stubs. Optional field — pre-2.1.3
1142 // engines that don't send it give an empty string, falling back to
1143 // the manifest-only behaviour.
1144 const auto tool_evidence = extract_json_string(
1145 context_json, "tool_evidence");
1146 const auto system_prompt = extract_json_string(
1147 context_json, "system_prompt");
1148 set_turn_context(tier, tool_context, tool_evidence, system_prompt);
1149
1150 auto result = validate(content, tier, nullptr);
1151 if (!result.was_revised) {
1152 return 0;
1153 }
1154
1155 write_modified_json(result.content, modified_json);
1156 return 0;
1157}
1158
1166void ConstitutionalValidator::write_modified_json(
1167 const std::string& content,
1168 char** modified_json) {
1169 std::string out = "{\"content\":\"";
1170 out += json_escape(content);
1171 out += "\"}";
1172
1173 auto* buf = static_cast<char*>(malloc(out.size() + 1));
1174 if (buf != nullptr) {
1175 std::memcpy(buf, out.c_str(), out.size() + 1);
1176 *modified_json = buf;
1177 }
1178}
1179
1180// ── Minimal JSON Field Extraction ─────────────────────────
1181// No nlohmann/json in core.so. These extract known fields from
1182// simple flat JSON objects by string search.
1183
1192std::string ConstitutionalValidator::extract_json_string(
1193 const char* json, const char* key) {
1194 if (json == nullptr || key == nullptr) {
1195 return {};
1196 }
1197
1198 std::string needle = std::string("\"") + key + "\"";
1199 const char* pos = strstr(json, needle.c_str());
1200 if (pos == nullptr) {
1201 return {};
1202 }
1203
1204 return extract_string_after_colon(pos + needle.size());
1205}
1206
1214std::string ConstitutionalValidator::extract_string_after_colon(
1215 const char* pos) {
1216 // Skip whitespace and colon
1217 while (*pos == ' ' || *pos == ':' || *pos == '\t') {
1218 ++pos;
1219 }
1220 if (*pos != '"') {
1221 return {};
1222 }
1223 ++pos; // skip opening quote
1224
1225 std::string value;
1226 while (*pos != '\0' && *pos != '"') {
1227 if (*pos == '\\' && *(pos + 1) != '\0') {
1228 ++pos;
1229 switch (*pos) {
1230 case 'n': value += '\n'; break;
1231 case 't': value += '\t'; break;
1232 case 'r': value += '\r'; break;
1233 default: value += *pos; break;
1234 }
1235 } else {
1236 value += *pos;
1237 }
1238 ++pos;
1239 }
1240 return value;
1241}
1242
1251bool ConstitutionalValidator::extract_compliant_field(
1252 const std::string& json, CritiqueResult& result) {
1253 auto pos = json.find("\"compliant\"");
1254 if (pos == std::string::npos) {
1255 logger->warn("Constitutional validation: malformed critique "
1256 "JSON — missing 'compliant' field");
1257 return false;
1258 }
1259
1260 result.compliant = (json.find("true", pos) < json.find("false", pos));
1261 return true;
1262}
1263
1271void ConstitutionalValidator::extract_violations(
1272 const std::string& json, CritiqueResult& result) {
1273 size_t search_pos = 0;
1274
1275 while (true) {
1276 auto v = extract_next_violation(json, search_pos);
1277 if (!v.has_value()) {
1278 break;
1279 }
1280 result.violations.push_back(std::move(v.value()));
1281 }
1282}
1283
1292std::optional<Violation>
1293ConstitutionalValidator::extract_next_violation(
1294 const std::string& json, size_t& pos) {
1295 auto rule_pos = json.find("\"rule\"", pos);
1296 if (rule_pos == std::string::npos) {
1297 return std::nullopt;
1298 }
1299
1300 Violation v;
1301 v.rule = extract_json_string(
1302 json.c_str() + rule_pos, "rule");
1303 v.excerpt = extract_json_string(
1304 json.c_str() + rule_pos, "excerpt");
1305 v.explanation = extract_json_string(
1306 json.c_str() + rule_pos, "explanation");
1307
1308 pos = rule_pos + 1;
1309 return v;
1310}
1311
1319void ConstitutionalValidator::extract_revised_field(
1320 const std::string& json, CritiqueResult& result) {
1321 result.revised = extract_json_string(json.c_str(), "revised");
1322}
1323
1324} // 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.
void append_turn_evidence(std::string &prompt) const
Append this turn's tool manifest and evidence to a prompt.
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).
@ enabled
Named in enabled_mcpjson_servers, or enable-all is set.
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.
@ optional
optional: every escape asks the host's path approver
@ tier
The TIER's identity frontmatter grammar: stem (gh#154).
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.
std::string config
–config FILE
Definition serve.cpp:38
Constitutional validation pipeline configuration.
Definition config.h:1177
int max_revisions
Max re-generation attempts (0 = critique only)
Definition config.h:1179
int priority
Hook priority (higher = later)
Definition config.h:1183
bool enable_thinking
Enable think-blocks for critique (default OFF)
Definition config.h:1182
float temperature
Critique generation temperature.
Definition config.h:1181
std::string critique_tier
Tier to route critique generation on.
Definition config.h:1191
int max_critique_tokens
Token budget for critique generation.
Definition config.h:1180
std::string grammar_key
Grammar registry key.
Definition config.h:1184
std::vector< std::string > skip_tiers
Tiers exempt from validation (default: lead — streams before hook fires)
Definition config.h:1186
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