38 const std::string& constitution_text)
40 constitution_text_(constitution_text),
41 global_enabled_(config.enabled) {
55 HookInterface* hook_iface,
56 InferenceInterface* inference) {
57 inference_ = inference;
60 if (hook_iface ==
nullptr || hook_iface->registry ==
nullptr) {
64 auto* reg =
static_cast<HookRegistry*
>(hook_iface->registry);
79 if (hook_iface ==
nullptr || hook_iface->registry ==
nullptr) {
82 auto* reg =
static_cast<HookRegistry*
>(hook_iface->registry);
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()) {
108 if (skip == identity_name) {
return false; }
110 return global_enabled_;
120 std::lock_guard<std::mutex> lock(overrides_mutex_);
121 global_enabled_ = enabled;
133 auto_retry_enabled_.store(enabled);
143 return auto_retry_enabled_.load();
158 std::optional<PendingValidationState> state;
160 std::lock_guard lock(pending_mutex_);
161 state = std::move(pending_state_);
162 pending_state_.reset();
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));
188 std::optional<PendingValidationState> state;
190 std::lock_guard lock(pending_mutex_);
191 state = std::move(pending_state_);
192 pending_state_.reset();
199 store_result(result);
200 logger->info(
"Constitutional validation accepted by consumer "
201 "(gh#30): attempt_n={}", result.
attempt_n);
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;
234 void (*start_cb)(
void*),
235 void (*end_cb)(
void*),
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;
251 const std::string& identity_name,
bool enabled) {
252 std::lock_guard<std::mutex> lock(overrides_mutex_);
253 identity_overrides_[identity_name] = enabled;
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;
286 const std::string& content,
287 const std::string& tier,
288 const char* messages_json) {
293 logger->info(
"Validation skipped for tier '{}'", tier);
295 store_result(result);
302 logger->info(
"Validation skipped: pure tool-call or empty");
304 store_result(result);
308 current_tier_ = tier;
309 logger->info(
"Validation start: {} chars, tier='{}'",
310 cleaned.size(), tier);
311 result = run_validation_loop(cleaned, tier, messages_json);
313 store_result(result);
323void ConstitutionalValidator::log_verdict(
327 logger->info(
"Validation passed (no violations)");
330 logger->info(
"Validation revised ({} revision(s) applied)",
334 logger->warn(
"Validation reverted "
335 "({} violation(s) found; revision discarded for length)",
339 logger->warn(
"Validation rejected "
340 "(max revisions exhausted; {} violation(s) remain)",
347 logger->info(
"Validation paused (gh#30): "
348 "{} violation(s); awaiting consumer",
352 logger->info(
"Validation accepted by consumer (gh#30): "
365 std::lock_guard<std::mutex> lock(result_mutex_);
383 const char* context_json,
384 char** modified_json,
386 *modified_json =
nullptr;
388 if (ctx ==
nullptr || ctx->validator ==
nullptr) {
392 return ctx->
validator->handle_hook(context_json, modified_json);
419 const std::string& content)
const {
421 prompt.reserve(constitution_text_.size() + content.size() + 512);
423 prompt +=
"You are a compliance evaluator. "
424 "Respond ONLY with the structured JSON evaluation.\n\n";
429 bool has_tier_rules =
false;
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";
441 prompt +=
"\nBackground constitutional guidance:\n";
442 prompt += constitution_text_;
444 prompt +=
"Constitutional Rules:\n";
445 prompt += constitution_text_;
450 if (!current_tool_context_.empty()) {
451 prompt +=
"\n\nTool calls made this turn:\n";
452 prompt += current_tool_context_;
461 if (!current_tool_evidence_.empty()) {
462 prompt +=
"\n\nTool result evidence (verify citations against this):\n";
463 prompt += current_tool_evidence_;
466 prompt +=
"\n\nEvaluate this output for compliance:\n\n---\n";
481 const std::string& json_str) {
485 if (!extract_compliant_field(json_str, result)) {
495 extract_violations(json_str, result);
496 extract_revised_field(json_str, result);
508void ConstitutionalValidator::store_result(
510 std::lock_guard<std::mutex> lock(result_mutex_);
511 last_result_ = result;
523ValidationResult ConstitutionalValidator::run_validation_loop(
524 const std::string& content,
525 const std::string& tier,
526 const char* messages_json) {
527 ValidationResult result;
530 auto critique = run_critique(content);
531 result.final_critique = critique;
533 if (critique.compliant) {
540 if (!auto_retry_enabled_.load()) {
542 result.attempt_n = 0;
544 std::lock_guard lock(pending_mutex_);
545 pending_state_ = PendingValidationState{
547 messages_json ? std::string(messages_json) : std::string{},
550 logger->info(
"Constitutional validation paused (gh#30): "
551 "auto_retry disabled, awaiting consumer decision");
555 return apply_revisions(result, critique, messages_json);
575ValidationResult ConstitutionalValidator::apply_revisions(
576 ValidationResult result,
577 const CritiqueResult& initial_critique,
578 const char* messages_json) {
579 auto critique = initial_critique;
581 const auto& before = result.content;
587 AttemptBoundaryCb cb_snap;
589 std::lock_guard<std::mutex> lk(attempt_boundary_mutex_);
590 cb_snap = attempt_boundary_;
592 if (cb_snap.cb !=
nullptr) {
594 cb_snap.cb(i + 1, cb_snap.user_data);
596 logger->warn(
"attempt_boundary_cb threw; swallowed at "
597 ".so boundary (gh#30)");
600 auto revised = attempt_revision(before, critique, messages_json);
603 if (
revised.size() < before.size() / 2) {
604 logger->warn(
"Constitutional validation: revision {}/{} "
605 "shrank content {}→{} chars (>50%); "
606 "discarding revision, returning original",
608 before.size(),
revised.size());
611 result.attempt_n = i + 1;
616 result.was_revised =
true;
617 result.revision_count = i + 1;
618 result.attempt_n = i + 1;
620 critique = run_critique(
revised);
621 result.final_critique = critique;
623 if (critique.compliant) {
break; }
626 if (!critique.compliant) {
627 logger->warn(
"Constitutional validation: max revisions ({}) "
628 "exhausted, returning last output",
631 }
else if (result.was_revised) {
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;
653 return revise(content, critique, messages_json);
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");
671 auto messages = build_critique_messages(content);
672 auto params = build_critique_params();
673 char* result_json =
nullptr;
680 CritiqueCallbacks cbs;
682 std::lock_guard<std::mutex> lock(critique_cbs_mutex_);
685 if (cbs.start_cb !=
nullptr) { cbs.start_cb(cbs.user_data); }
687 int rc = inference_->generate(
688 messages.c_str(), params.c_str(),
689 &result_json, inference_->backend_data);
691 if (cbs.end_cb !=
nullptr) { cbs.end_cb(cbs.user_data); }
693 if (rc != 0 || result_json ==
nullptr) {
694 logger->warn(
"Constitutional validation: critique generation "
695 "failed (rc={})", rc);
699 std::string raw(result_json);
700 if (inference_->free_fn !=
nullptr) {
701 inference_->free_fn(result_json);
714std::string ConstitutionalValidator::build_critique_messages(
715 const std::string& content)
const {
717 return build_single_turn_json(prompt);
726std::string ConstitutionalValidator::build_critique_params()
const {
727 std::string params =
"{\"grammar_key\":\"";
729 params +=
"\",\"max_tokens\":";
731 params +=
",\"temperature\":";
733 params +=
",\"enable_thinking\":";
740 params +=
",\"tier\":\"";
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) {
772 auto augmented = build_revision_messages(
773 original, critique, messages_json);
774 char* result_json =
nullptr;
778 int rc = inference_->generate(
779 augmented.c_str(),
"{}", &result_json,
780 inference_->backend_data);
782 if (rc != 0 || result_json ==
nullptr) {
783 logger->warn(
"Constitutional validation: revision generation "
784 "failed (rc={})", rc);
788 std::string
revised(result_json);
789 if (inference_->free_fn !=
nullptr) {
790 inference_->free_fn(result_json);
807 out.reserve(s.size() + 16);
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;
828std::string ConstitutionalValidator::build_single_turn_json(
829 const std::string& prompt)
const {
830 std::string json =
"[{\"role\":\"user\",\"content\":\"";
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);
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";
868 feedback +=
"Please revise your response to comply with all "
869 "constitutional rules.";
888std::string ConstitutionalValidator::inject_feedback_into_messages(
889 const std::string& original,
890 const std::string& feedback,
891 const char* messages_json)
const {
893 if (messages_json !=
nullptr) {
894 base = std::string(messages_json);
898 const auto& sys = !current_system_prompt_.empty()
899 ? current_system_prompt_ : constitution_text_;
900 base =
"[{\"role\":\"system\",\"content\":\""
905 if (!base.empty() && base.back() ==
']') {
910 if (base.size() > 1) {
914 base +=
"{\"role\":\"assistant\",\"content\":\"";
916 base +=
"\"},{\"role\":\"system\",\"content\":\"";
917 base +=
json_escape(
"[CONSTITUTIONAL REVIEW] " + feedback);
937 while (pos < content.size()) {
938 auto open = content.find(
"<think>", pos);
939 if (open == std::string::npos) {
940 result.append(content, pos);
943 result.append(content, pos, open - pos);
944 auto close = content.find(
"</think>", open);
945 pos = (close == std::string::npos)
965 std::string stripped = content;
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);
975 return stripped.find_first_not_of(
" \t\n\r") == std::string::npos;
992int ConstitutionalValidator::handle_hook(
993 const char* context_json,
994 char** modified_json) {
995 *modified_json =
nullptr;
997 auto content = extract_json_string(context_json,
"content");
998 auto tier = extract_json_string(context_json,
"tier");
1000 if (content.empty()) {
return 0; }
1003 current_tool_context_ = extract_json_string(
1004 context_json,
"tool_context");
1010 current_tool_evidence_ = extract_json_string(
1011 context_json,
"tool_evidence");
1012 current_system_prompt_ = extract_json_string(
1013 context_json,
"system_prompt");
1015 auto result =
validate(content, tier,
nullptr);
1016 if (!result.was_revised) {
1020 write_modified_json(result.content, modified_json);
1031void ConstitutionalValidator::write_modified_json(
1032 const std::string& content,
1033 char** modified_json) {
1034 std::string out =
"{\"content\":\"";
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;
1057std::string ConstitutionalValidator::extract_json_string(
1058 const char* json,
const char* key) {
1059 if (json ==
nullptr || key ==
nullptr) {
1063 std::string needle = std::string(
"\"") + key +
"\"";
1064 const char* pos = strstr(json, needle.c_str());
1065 if (pos ==
nullptr) {
1069 return extract_string_after_colon(pos + needle.size());
1079std::string ConstitutionalValidator::extract_string_after_colon(
1082 while (*pos ==
' ' || *pos ==
':' || *pos ==
'\t') {
1091 while (*pos !=
'\0' && *pos !=
'"') {
1092 if (*pos ==
'\\' && *(pos + 1) !=
'\0') {
1095 case 'n': value +=
'\n';
break;
1096 case 't': value +=
'\t';
break;
1097 case 'r': value +=
'\r';
break;
1098 default: value += *pos;
break;
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");
1125 result.compliant = (json.find(
"true", pos) < json.find(
"false", pos));
1136void ConstitutionalValidator::extract_violations(
1137 const std::string& json, CritiqueResult& result) {
1138 size_t search_pos = 0;
1141 auto v = extract_next_violation(json, search_pos);
1142 if (!v.has_value()) {
1145 result.violations.push_back(std::move(v.value()));
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;
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");
1184void ConstitutionalValidator::extract_revised_field(
1185 const std::string& json, CritiqueResult& result) {
1186 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_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).
@ 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.
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.