42const std::string& ConstitutionalValidator::current_tier() {
52const std::string& ConstitutionalValidator::current_tool_context() {
62const std::string& ConstitutionalValidator::current_tool_evidence() {
72const std::string& ConstitutionalValidator::current_system_prompt() {
85void ConstitutionalValidator::set_turn_context(
86 const std::string&
tier,
89 const std::string& system_prompt) {
103 const std::string& open,
104 const std::string& close);
133 inference_ = inference;
177 std::lock_guard<std::mutex> lock(overrides_mutex_);
179 if (
it != identity_overrides_.end()) {
186 return global_enabled_;
196 std::lock_guard<std::mutex> lock(overrides_mutex_);
207 std::function<std::pair<std::string, std::string>(
const std::string&)>
209 std::lock_guard<std::mutex> lock(marker_mutex_);
210 marker_resolver_ = std::move(
resolver);
222 auto_retry_enabled_.store(
enabled);
232 return auto_retry_enabled_.load();
247 std::optional<PendingValidationState> state;
249 std::lock_guard lock(pending_mutex_);
250 state = std::move(pending_state_);
251 pending_state_.reset();
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));
281 std::optional<PendingValidationState> state;
283 std::lock_guard lock(pending_mutex_);
284 state = std::move(pending_state_);
285 pending_state_.reset();
292 store_result(result);
293 logger->info(
"Constitutional validation accepted by consumer "
294 "(gh#30): attempt_n={}", result.
attempt_n);
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;
327 void (*start_cb)(
void*),
328 void (*end_cb)(
void*),
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;
345 std::lock_guard<std::mutex> lock(overrides_mutex_);
358 const std::vector<std::string>&
rules) {
359 std::lock_guard<std::mutex> lock(overrides_mutex_);
380 const std::string& content,
381 const std::string&
tier,
382 const char* messages_json) {
387 logger->info(
"Validation skipped for tier '{}'",
tier);
389 store_result(result);
395 std::string open =
"<think>";
396 std::string close =
"</think>";
398 std::lock_guard<std::mutex> lock(marker_mutex_);
399 if (marker_resolver_) { std::tie(open, close) = marker_resolver_(
tier); }
403 logger->info(
"Validation skipped: pure tool-call or empty");
405 store_result(result);
410 logger->info(
"Validation start: {} chars, tier='{}'",
412 result = run_validation_loop(
cleaned,
tier, messages_json);
414 store_result(result);
424void ConstitutionalValidator::log_verdict(
428 logger->info(
"Validation passed (no violations)");
431 logger->info(
"Validation revised ({} revision(s) applied)",
435 logger->warn(
"Validation reverted "
436 "({} violation(s) found; revision discarded for length)",
440 logger->warn(
"Validation rejected "
441 "(max revisions exhausted; {} violation(s) remain)",
448 logger->info(
"Validation paused (gh#30): "
449 "{} violation(s); awaiting consumer",
453 logger->info(
"Validation accepted by consumer (gh#30): "
466 std::lock_guard<std::mutex> lock(result_mutex_);
489 if (
ctx ==
nullptr ||
ctx->validator ==
nullptr) {
520 const std::string& content)
const {
522 prompt.reserve(constitution_text_.size() + content.size() + 512);
524 prompt +=
"You are a compliance evaluator. "
525 "Respond ONLY with the structured JSON evaluation.\n\n";
532 std::lock_guard<std::mutex> lock(overrides_mutex_);
533 auto it = tier_rules_.find(current_tier());
535 && !
it->second.empty());
537 prompt +=
"Evaluate against these rules for the '"
538 + current_tier() +
"' identity:\n";
539 for (
const auto& rule :
it->second) {
540 prompt +=
"- " + rule +
"\n";
542 prompt +=
"\nBackground constitutional guidance:\n";
543 prompt += constitution_text_;
545 prompt +=
"Constitutional Rules:\n";
546 prompt += constitution_text_;
552 prompt +=
"\n\nEvaluate this output for compliance:\n\n---\n";
577 std::string& prompt)
const {
578 const std::string&
manifest = current_tool_context();
580 prompt +=
"\n\nTool calls made this turn:\n";
583 const std::string&
evidence = current_tool_evidence();
585 prompt +=
"\n\nTool result evidence (verify citations against this):\n";
603 if (!extract_compliant_field(
json_str, result)) {
613 extract_violations(
json_str, result);
614 extract_revised_field(
json_str, result);
626void ConstitutionalValidator::store_result(
628 std::lock_guard<std::mutex> lock(result_mutex_);
629 last_result_ = result;
642ValidationResult ConstitutionalValidator::run_validation_loop(
643 const std::string& content,
644 const std::string&
tier,
645 const char* messages_json) {
646 ValidationResult result;
649 auto critique = run_critique(content);
650 result.final_critique = critique;
652 if (critique.compliant) {
659 if (!auto_retry_enabled_.load()) {
661 result.attempt_n = 0;
663 std::lock_guard lock(pending_mutex_);
664 pending_state_ = PendingValidationState{
666 messages_json ? std::string(messages_json) :
std::
string{},
669 logger->info(
"Constitutional validation paused (gh#30): "
670 "auto_retry disabled, awaiting consumer decision");
674 return apply_revisions(result, critique, messages_json);
695ValidationResult ConstitutionalValidator::apply_revisions(
696 ValidationResult result,
698 const char* messages_json) {
701 const auto&
before = result.content;
709 std::lock_guard<std::mutex>
lk(attempt_boundary_mutex_);
716 logger->warn(
"attempt_boundary_cb threw; swallowed at "
717 ".so boundary (gh#30)");
720 auto revised = attempt_revision(
before, critique, messages_json);
724 logger->warn(
"Constitutional validation: revision {}/{} "
725 "shrank content {}→{} chars (>50%); "
726 "discarding revision, returning original",
731 result.attempt_n =
i + 1;
736 result.was_revised =
true;
737 result.revision_count =
i + 1;
738 result.attempt_n =
i + 1;
740 critique = run_critique(
revised);
741 result.final_critique = critique;
743 if (critique.compliant) {
break; }
746 if (!critique.compliant) {
747 logger->warn(
"Constitutional validation: max revisions ({}) "
748 "exhausted, returning last output",
751 }
else if (result.was_revised) {
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;
773 return revise(content, critique, messages_json);
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");
792 auto messages = build_critique_messages(content);
793 auto params = build_critique_params();
801 CritiqueCallbacks
cbs;
803 std::lock_guard<std::mutex> lock(critique_cbs_mutex_);
806 if (
cbs.start_cb !=
nullptr) {
cbs.start_cb(
cbs.user_data); }
808 int rc = inference_->generate(
809 messages.c_str(),
params.c_str(),
812 if (
cbs.end_cb !=
nullptr) {
cbs.end_cb(
cbs.user_data); }
815 logger->warn(
"Constitutional validation: critique generation "
816 "failed (rc={})",
rc);
821 if (inference_->free_fn !=
nullptr) {
835std::string ConstitutionalValidator::build_critique_messages(
836 const std::string& content)
const {
838 return build_single_turn_json(prompt);
847std::string ConstitutionalValidator::build_critique_params()
const {
848 std::string
params =
"{\"grammar_key\":\"";
850 params +=
"\",\"max_tokens\":";
852 params +=
",\"temperature\":";
854 params +=
",\"enable_thinking\":";
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) {
893 auto augmented = build_revision_messages(
894 original, critique, messages_json);
899 int rc = inference_->generate(
901 inference_->backend_data);
904 logger->warn(
"Constitutional validation: revision generation "
905 "failed (rc={})",
rc);
910 if (inference_->free_fn !=
nullptr) {
928 out.reserve(
s.size() + 16);
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;
949std::string ConstitutionalValidator::build_single_turn_json(
950 const std::string& prompt)
const {
951 std::string json =
"[{\"role\":\"user\",\"content\":\"";
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(
982std::string ConstitutionalValidator::build_feedback_text(
983 const CritiqueResult& critique)
const {
985 "Your response violated these constitutional rules:\\n";
986 for (
const auto&
v : critique.violations) {
987 feedback +=
"- " +
v.rule +
": " +
v.explanation +
"\\n";
989 feedback +=
"Please revise your response to comply with all "
990 "constitutional rules.";
1009std::string ConstitutionalValidator::inject_feedback_into_messages(
1010 const std::string& original,
1012 const char* messages_json)
const {
1014 if (messages_json !=
nullptr) {
1015 base = std::string(messages_json);
1019 const auto&
sys = !current_system_prompt().empty()
1020 ? current_system_prompt() : constitution_text_;
1021 base =
"[{\"role\":\"system\",\"content\":\""
1026 if (!base.empty() && base.back() ==
']') {
1031 if (base.size() > 1) {
1035 base +=
"{\"role\":\"assistant\",\"content\":\"";
1037 base +=
"\"},{\"role\":\"system\",\"content\":\"";
1068 const std::string& open,
1069 const std::string& close) {
1072 while (pos < content.size()) {
1073 auto o = content.find(open, pos);
1074 if (
o == std::string::npos) {
1075 result.append(content, pos);
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();
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);
1108 return stripped.find_first_not_of(
" \t\n\r") == std::string::npos;
1126int ConstitutionalValidator::handle_hook(
1131 auto content = extract_json_string(
context_json,
"content");
1134 if (content.empty()) {
return 0; }
1146 const auto system_prompt = extract_json_string(
1151 if (!result.was_revised) {
1166void ConstitutionalValidator::write_modified_json(
1167 const std::string& content,
1169 std::string out =
"{\"content\":\"";
1173 auto*
buf =
static_cast<char*
>(
malloc(out.size() + 1));
1174 if (
buf !=
nullptr) {
1175 std::memcpy(
buf, out.c_str(), out.size() + 1);
1192std::string ConstitutionalValidator::extract_json_string(
1193 const char* json,
const char* key) {
1194 if (json ==
nullptr || key ==
nullptr) {
1198 std::string
needle = std::string(
"\"") + key +
"\"";
1200 if (pos ==
nullptr) {
1204 return extract_string_after_colon(pos +
needle.size());
1214std::string ConstitutionalValidator::extract_string_after_colon(
1217 while (*pos ==
' ' || *pos ==
':' || *pos ==
'\t') {
1226 while (*pos !=
'\0' && *pos !=
'"') {
1227 if (*pos ==
'\\' && *(pos + 1) !=
'\0') {
1230 case 'n':
value +=
'\n';
break;
1231 case 't':
value +=
'\t';
break;
1232 case 'r':
value +=
'\r';
break;
1233 default:
value += *pos;
break;
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");
1260 result.compliant = (json.find(
"true", pos) < json.find(
"false", pos));
1271void ConstitutionalValidator::extract_violations(
1272 const std::string& json, CritiqueResult& result) {
1276 auto v = extract_next_violation(json,
search_pos);
1277 if (!
v.has_value()) {
1280 result.violations.push_back(std::move(
v.value()));
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;
1301 v.rule = extract_json_string(
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");
1319void ConstitutionalValidator::extract_revised_field(
1320 const std::string& json, CritiqueResult& result) {
1321 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.
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.
@ 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).
@ 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
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.