Class: Insika::Safety::OutputValidator

Inherits:
Object
  • Object
show all
Defined in:
lib/insika/safety/output_validator.rb

Overview

Post-turn output validator (RFC-0009 §3.2 / D3). Runs as an after_task hook: with the FINAL assistant text assembled, it does a richer check than the stream filter can — an unverified discount/price promise, a tone slip — and FLAGS it (audit) rather than blocking. It is honest about the streaming limit (D3): for a streaming agent the text is already out the door, so this is detection, not prevention; true pre-emission blocking needs streaming:false (a per-profile override left for a later slice).

Contract: as an after_task hook it receives the TurnState (the :task subject), appends any finding to state.guardrail_flags, and returns the state unchanged. The Executor (single emitter) turns each flag into a :guardrail_flagged event after the turn — the hook itself never emits.

Two tiers, like the input side:

1. deterministic — residual PII/secret in the final text (a belt to the
 stream filter's suspenders) via Detectors;
2. LLM validator (opt-in, same moderator model) — promise/tone judgment the
 regex can't make. Pure over an injected `ask`; fail-open on any error.

Instance Method Summary collapse

Constructor Details

#initialize(ask_factory: nil) ⇒ OutputValidator

ask_factory (optional): ->(config) { ->(prompt)text | nil }, built by the Safety::Factory from the utility_model. nil = deterministic only.



30
31
32
# File 'lib/insika/safety/output_validator.rb', line 30

def initialize(ask_factory: nil)
  @ask_factory = ask_factory
end

Instance Method Details

#call(state) ⇒ Object

after_task hook body. Idempotent and defensive: never raises out (a hook error must not fail a committed turn).



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/insika/safety/output_validator.rb', line 36

def call(state)
  config = Config.from_profile(state.profile)
  return state unless config.output

  text = state.response_content.to_s
  return state if text.empty?

  flags = []
  flags.concat(deterministic_flags(text))
  flags.concat(llm_flags(text, config)) if config.moderator?

  state.guardrail_flags = Array(state.guardrail_flags) + flags unless flags.empty?
  state
rescue StandardError
  state # fail-open: auditing must never break a completed turn
end