29 const std::string& open,
30 const std::string& close);
40 const std::string& constitution_text)
42 constitution_text_(constitution_text),
43 global_enabled_(config.enabled) {
57 HookInterface* hook_iface,
58 InferenceInterface* inference) {
59 inference_ = inference;
62 if (hook_iface ==
nullptr || hook_iface->registry ==
nullptr) {
66 auto* reg =
static_cast<HookRegistry*
>(hook_iface->registry);
81 if (hook_iface ==
nullptr || hook_iface->registry ==
nullptr) {
84 auto* reg =
static_cast<HookRegistry*
>(hook_iface->registry);
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()) {
110 if (skip == identity_name) {
return false; }
112 return global_enabled_;
122 std::lock_guard<std::mutex> lock(overrides_mutex_);
123 global_enabled_ = enabled;
133 std::function<std::pair<std::string, std::string>(
const std::string&)>
135 std::lock_guard<std::mutex> lock(marker_mutex_);
136 marker_resolver_ = std::move(resolver);
148 auto_retry_enabled_.store(enabled);
158 return auto_retry_enabled_.load();
173 std::optional<PendingValidationState> state;
175 std::lock_guard lock(pending_mutex_);
176 state = std::move(pending_state_);
177 pending_state_.reset();
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));
203 std::optional<PendingValidationState> state;
205 std::lock_guard lock(pending_mutex_);
206 state = std::move(pending_state_);
207 pending_state_.reset();
214 store_result(result);
215 logger->info(
"Constitutional validation accepted by consumer "
216 "(gh#30): attempt_n={}", result.
attempt_n);
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;
249 void (*start_cb)(
void*),
250 void (*end_cb)(
void*),
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;
266 const std::string& identity_name,
bool enabled) {
267 std::lock_guard<std::mutex> lock(overrides_mutex_);
268 identity_overrides_[identity_name] = enabled;
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;
302 const std::string& content,
303 const std::string& tier,
304 const char* messages_json) {
309 logger->info(
"Validation skipped for tier '{}'", tier);
311 store_result(result);
317 std::string open =
"<think>";
318 std::string close =
"</think>";
320 std::lock_guard<std::mutex> lock(marker_mutex_);
321 if (marker_resolver_) { std::tie(open, close) = marker_resolver_(tier); }
325 logger->info(
"Validation skipped: pure tool-call or empty");
327 store_result(result);
331 current_tier_ = tier;
332 logger->info(
"Validation start: {} chars, tier='{}'",
333 cleaned.size(), tier);
334 result = run_validation_loop(cleaned, tier, messages_json);
336 store_result(result);
346void ConstitutionalValidator::log_verdict(
350 logger->info(
"Validation passed (no violations)");
353 logger->info(
"Validation revised ({} revision(s) applied)",
357 logger->warn(
"Validation reverted "
358 "({} violation(s) found; revision discarded for length)",
362 logger->warn(
"Validation rejected "
363 "(max revisions exhausted; {} violation(s) remain)",
370 logger->info(
"Validation paused (gh#30): "
371 "{} violation(s); awaiting consumer",
375 logger->info(
"Validation accepted by consumer (gh#30): "
388 std::lock_guard<std::mutex> lock(result_mutex_);
406 const char* context_json,
407 char** modified_json,
409 *modified_json =
nullptr;
411 if (ctx ==
nullptr || ctx->validator ==
nullptr) {
415 return ctx->
validator->handle_hook(context_json, modified_json);
442 const std::string& content)
const {
444 prompt.reserve(constitution_text_.size() + content.size() + 512);
446 prompt +=
"You are a compliance evaluator. "
447 "Respond ONLY with the structured JSON evaluation.\n\n";
452 bool has_tier_rules =
false;
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";
464 prompt +=
"\nBackground constitutional guidance:\n";
465 prompt += constitution_text_;
467 prompt +=
"Constitutional Rules:\n";
468 prompt += constitution_text_;
473 if (!current_tool_context_.empty()) {
474 prompt +=
"\n\nTool calls made this turn:\n";
475 prompt += current_tool_context_;
484 if (!current_tool_evidence_.empty()) {
485 prompt +=
"\n\nTool result evidence (verify citations against this):\n";
486 prompt += current_tool_evidence_;
489 prompt +=
"\n\nEvaluate this output for compliance:\n\n---\n";
504 const std::string& json_str) {
508 if (!extract_compliant_field(json_str, result)) {
518 extract_violations(json_str, result);
519 extract_revised_field(json_str, result);
531void ConstitutionalValidator::store_result(
533 std::lock_guard<std::mutex> lock(result_mutex_);
534 last_result_ = result;
547ValidationResult ConstitutionalValidator::run_validation_loop(
548 const std::string& content,
549 const std::string& tier,
550 const char* messages_json) {
551 ValidationResult result;
554 auto critique = run_critique(content);
555 result.final_critique = critique;
557 if (critique.compliant) {
564 if (!auto_retry_enabled_.load()) {
566 result.attempt_n = 0;
568 std::lock_guard lock(pending_mutex_);
569 pending_state_ = PendingValidationState{
571 messages_json ? std::string(messages_json) : std::string{},
574 logger->info(
"Constitutional validation paused (gh#30): "
575 "auto_retry disabled, awaiting consumer decision");
579 return apply_revisions(result, critique, messages_json);
600ValidationResult ConstitutionalValidator::apply_revisions(
601 ValidationResult result,
602 const CritiqueResult& initial_critique,
603 const char* messages_json) {
604 auto critique = initial_critique;
606 const auto& before = result.content;
612 AttemptBoundaryCb cb_snap;
614 std::lock_guard<std::mutex> lk(attempt_boundary_mutex_);
615 cb_snap = attempt_boundary_;
617 if (cb_snap.cb !=
nullptr) {
619 cb_snap.cb(i + 1, cb_snap.user_data);
621 logger->warn(
"attempt_boundary_cb threw; swallowed at "
622 ".so boundary (gh#30)");
625 auto revised = attempt_revision(before, critique, messages_json);
628 if (
revised.size() < before.size() / 2) {
629 logger->warn(
"Constitutional validation: revision {}/{} "
630 "shrank content {}→{} chars (>50%); "
631 "discarding revision, returning original",
633 before.size(),
revised.size());
636 result.attempt_n = i + 1;
641 result.was_revised =
true;
642 result.revision_count = i + 1;
643 result.attempt_n = i + 1;
645 critique = run_critique(
revised);
646 result.final_critique = critique;
648 if (critique.compliant) {
break; }
651 if (!critique.compliant) {
652 logger->warn(
"Constitutional validation: max revisions ({}) "
653 "exhausted, returning last output",
656 }
else if (result.was_revised) {
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;
678 return revise(content, critique, messages_json);
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");
697 auto messages = build_critique_messages(content);
698 auto params = build_critique_params();
699 char* result_json =
nullptr;
706 CritiqueCallbacks cbs;
708 std::lock_guard<std::mutex> lock(critique_cbs_mutex_);
711 if (cbs.start_cb !=
nullptr) { cbs.start_cb(cbs.user_data); }
713 int rc = inference_->generate(
714 messages.c_str(), params.c_str(),
715 &result_json, inference_->backend_data);
717 if (cbs.end_cb !=
nullptr) { cbs.end_cb(cbs.user_data); }
719 if (rc != 0 || result_json ==
nullptr) {
720 logger->warn(
"Constitutional validation: critique generation "
721 "failed (rc={})", rc);
725 std::string raw(result_json);
726 if (inference_->free_fn !=
nullptr) {
727 inference_->free_fn(result_json);
740std::string ConstitutionalValidator::build_critique_messages(
741 const std::string& content)
const {
743 return build_single_turn_json(prompt);
752std::string ConstitutionalValidator::build_critique_params()
const {
753 std::string params =
"{\"grammar_key\":\"";
755 params +=
"\",\"max_tokens\":";
757 params +=
",\"temperature\":";
759 params +=
",\"enable_thinking\":";
766 params +=
",\"tier\":\"";
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) {
798 auto augmented = build_revision_messages(
799 original, critique, messages_json);
800 char* result_json =
nullptr;
804 int rc = inference_->generate(
805 augmented.c_str(),
"{}", &result_json,
806 inference_->backend_data);
808 if (rc != 0 || result_json ==
nullptr) {
809 logger->warn(
"Constitutional validation: revision generation "
810 "failed (rc={})", rc);
814 std::string
revised(result_json);
815 if (inference_->free_fn !=
nullptr) {
816 inference_->free_fn(result_json);
833 out.reserve(s.size() + 16);
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;
854std::string ConstitutionalValidator::build_single_turn_json(
855 const std::string& prompt)
const {
856 std::string json =
"[{\"role\":\"user\",\"content\":\"";
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);
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";
894 feedback +=
"Please revise your response to comply with all "
895 "constitutional rules.";
914std::string ConstitutionalValidator::inject_feedback_into_messages(
915 const std::string& original,
916 const std::string& feedback,
917 const char* messages_json)
const {
919 if (messages_json !=
nullptr) {
920 base = std::string(messages_json);
924 const auto& sys = !current_system_prompt_.empty()
925 ? current_system_prompt_ : constitution_text_;
926 base =
"[{\"role\":\"system\",\"content\":\""
931 if (!base.empty() && base.back() ==
']') {
936 if (base.size() > 1) {
940 base +=
"{\"role\":\"assistant\",\"content\":\"";
942 base +=
"\"},{\"role\":\"system\",\"content\":\"";
943 base +=
json_escape(
"[CONSTITUTIONAL REVIEW] " + feedback);
973 const std::string& open,
974 const std::string& close) {
977 while (pos < content.size()) {
978 auto o = content.find(open, pos);
979 if (o == std::string::npos) {
980 result.append(content, pos);
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();
1003 std::string stripped = content;
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);
1013 return stripped.find_first_not_of(
" \t\n\r") == std::string::npos;
1031int ConstitutionalValidator::handle_hook(
1032 const char* context_json,
1033 char** modified_json) {
1034 *modified_json =
nullptr;
1036 auto content = extract_json_string(context_json,
"content");
1037 auto tier = extract_json_string(context_json,
"tier");
1039 if (content.empty()) {
return 0; }
1042 current_tool_context_ = extract_json_string(
1043 context_json,
"tool_context");
1049 current_tool_evidence_ = extract_json_string(
1050 context_json,
"tool_evidence");
1051 current_system_prompt_ = extract_json_string(
1052 context_json,
"system_prompt");
1054 auto result =
validate(content, tier,
nullptr);
1055 if (!result.was_revised) {
1059 write_modified_json(result.content, modified_json);
1070void ConstitutionalValidator::write_modified_json(
1071 const std::string& content,
1072 char** modified_json) {
1073 std::string out =
"{\"content\":\"";
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;
1096std::string ConstitutionalValidator::extract_json_string(
1097 const char* json,
const char* key) {
1098 if (json ==
nullptr || key ==
nullptr) {
1102 std::string needle = std::string(
"\"") + key +
"\"";
1103 const char* pos = strstr(json, needle.c_str());
1104 if (pos ==
nullptr) {
1108 return extract_string_after_colon(pos + needle.size());
1118std::string ConstitutionalValidator::extract_string_after_colon(
1121 while (*pos ==
' ' || *pos ==
':' || *pos ==
'\t') {
1130 while (*pos !=
'\0' && *pos !=
'"') {
1131 if (*pos ==
'\\' && *(pos + 1) !=
'\0') {
1134 case 'n': value +=
'\n';
break;
1135 case 't': value +=
'\t';
break;
1136 case 'r': value +=
'\r';
break;
1137 default: value += *pos;
break;
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");
1164 result.compliant = (json.find(
"true", pos) < json.find(
"false", pos));
1175void ConstitutionalValidator::extract_violations(
1176 const std::string& json, CritiqueResult& result) {
1177 size_t search_pos = 0;
1180 auto v = extract_next_violation(json, search_pos);
1181 if (!v.has_value()) {
1184 result.violations.push_back(std::move(v.value()));
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;
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");
1223void ConstitutionalValidator::extract_revised_field(
1224 const std::string& json, CritiqueResult& result) {
1225 result.revised = extract_json_string(json.c_str(),
"revised");
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.
@ ENTROPIC_ERROR_INVALID_ARGUMENT
NULL pointer, empty string, out-of-range value.
@ ENTROPIC_ERROR_INVALID_STATE
Operation not valid in current state (e.g., generate before activate)
Thread-safe hook registration and dispatch.
entropic_hook_point_t
Hook points in the engine lifecycle.
@ ENTROPIC_HOOK_POST_GENERATE
1: After inference generate returns
spdlog initialization and logger access.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Activate model on GPU (WARM → ACTIVE).
static std::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.
int max_revisions
Max re-generation attempts (0 = critique only)
int priority
Hook priority (higher = later)
bool enable_thinking
Enable think-blocks for critique (default OFF)
float temperature
Critique generation temperature.
std::string critique_tier
Tier to route critique generation on.
int max_critique_tokens
Token budget for critique generation.
std::string grammar_key
Grammar registry key.
std::vector< std::string > skip_tiers
Tiers exempt from validation (default: lead — streams before hook fires)
Structured result from a single critique generation pass.
std::vector< Violation > violations
List of constitutional violations.
std::string raw_json
Raw critique JSON for audit logging.
bool compliant
true if output passes all rules
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.
ValidationVerdict verdict
Structured outcome (2.0.6-rc17)
CritiqueResult final_critique
Last critique result.
std::string content
Final output (original or revised)
int revision_count
Number of revision attempts made.