@validator Hook Helper
What it is and when to use it
A validator is a predicate. It takes the return value of a skill and gives back a Result(ok, value, feedback). Think of it as the bridge that lets the agent say "please try that again" to the LLM. Sometimes a skill call is syntactically fine but wrong on the merits: an ISBN checksum that does not pass, an answer that runs over a length limit, a business rule that was not satisfied. In those cases the validator writes a plain description of the problem into the feedback field of Result.failure(...). The agent loop wraps that into a ValidationErrorObservation(validator_name=..., feedback=...) and feeds it back to the model, so the next turn has a chance to correct the mistake.
Since v0.3.0, a validator is not registered in the registry. It is a hook helper. You bind it to a particular skill with @skill(post_hook=...), and it runs after the skill body returns successfully but before the SkillObservation is written into the EventStream. A common case: you write get_summary(topic) that returns a string and attach a non_empty(text) validator to make sure the string is not empty. If it returns Result.failure("empty"), the agent loop receives ValidationErrorObservation(validator_name="non_empty", feedback="empty"), and on the next turn the model sees that feedback and regenerates the answer itself.
Import everything from cantus.hooks:
from cantus.hooks import validator, Validator, ResultA validator does not repair data. Its job is to judge and to give feedback, nothing more. If you find yourself wanting to mutate the input inside a validator, what you actually want is an analyzer or a separate skill.
Two ways to write it (the same ensure_isbn_valid)
1. Decorator entry (the common case)
from cantus import skill
from cantus.hooks import validator, Result
@validator
def ensure_isbn_valid(book: Book) -> Result:
"""Verify the ISBN-13 checksum."""
if checksum_ok(book.isbn):
return Result.success(book)
return Result.failure("ISBN checksum mismatch — re-check the digits.")
@skill(post_hook=ensure_isbn_valid)
def fetch_book(title: str) -> Book:
"""Look up a book by title."""
return _do_fetch(title)2. Class-first (advanced / canonical)
from cantus.hooks import Validator, Result
class EnsureIsbnValid(Validator):
"""Verify the ISBN-13 checksum."""
name = "ensure_isbn_valid"
def run(self, book: Book) -> Result:
if checksum_ok(book.isbn):
return Result.success(book)
return Result.failure("ISBN checksum mismatch — re-check the digits.")
ensure_isbn_valid = EnsureIsbnValid()The class-first form suits cases where the validator needs to carry state of its own: a rule version, a tolerance, a reference to an external schema. Under the hood, the decorator form also ends up synthesizing an equivalent subclass.
v0.3.0 exposes no function-pass entry: there is no
register_validator(fn)in thecantus.hookspublic surface.
spec_for_llm() and dispatch behavior
- Like an analyzer, a validator does not appear directly in the LLM's system prompt. It is attached to a skill, and that skill's spec JSON keeps the same shape — still just the three keys
{"name", "description", "args_schema"}. - The post-hook runs after the skill body returns successfully, taking the skill's return value as its input.
Result(ok=True, value=v)producesSkillObservation(result=v).Result(ok=True)with novaluefalls back to the skill's original return value.Result(ok=False, feedback=...)producesValidationErrorObservation(validator_name="<post_hook function name>", feedback=...)and emits noSkillObservation.- A return value that is not a
Resultis written straight intoSkillObservationas the skill's newresult. So a post-hook can also tidy up formatting in passing. When you want a strict pass/fail decision, always return aResult. - If the post-hook raises, you get
ToolErrorObservation(message="post_hook <ExcType>: <msg>").
Common mistakes
- Forgetting to return a
Result. A post-hook that returnsTrue,book, orNonehas that value passed through as the new result. To get a strict decision, you must returnResult.success(...)orResult.failure(...). - Writing feedback like an engineer.
"AssertionError at line 42"means nothing to the LLM. Write an instruction the model can act on, such as"An ISBN must be 13 digits; only 10 are present, so add the missing digits." - Using a validator as a fixer. Quietly patching the value inside the post-hook and then returning
Result.success(...)is an anti-pattern. Move data repair into an analyzer or a new skill. - Trying
from cantus import validatororregister_validator(fn). Both raiseImportError. Usefrom cantus.hooks import validatortogether with@skill(post_hook=fn). - Taking a reserved name. A validator name must not collide with
RESERVED_VALIDATOR_NAMES, or it raisesReservedValidatorNameError.