Write a guardrail
This guardrail keeps an agent's answers short: it fails a turn whose answer is longer than a limit.
The guardrail
Section titled “The guardrail”import { defineCheck } from '@kindgi/sdk/define';import { z } from 'zod';
export const check = defineCheck({ id: 'my-pack.checks.answer-length', kind: 'zero-llm', configSchema: z.object({ maxChars: z.number().int().positive().default(500) }), evaluate: async (config, trace) => { const length = trace.output?.length ?? 0; if (length > config.maxChars) { return { passed: false, reason: `The answer is ${length} characters; the limit is ${config.maxChars}.`, }; } return { passed: true }; },});
export default { id: 'my-pack.answer-length', name: 'Answer is short', kind: 'zero-llm', check, config: { maxChars: 60 }, action: { 'on-violation': 'halt' }, severity: 'error',};The file has two parts:
- The check, from
defineCheck, exported ascheck. Itsevaluategets the guardrail's config and the turn's trace, and returns{ passed, reason? }. - The declaration, the default export: the guardrail's id, the check it
runs, the check's
config, and theactionon a failure.
from pydantic import BaseModel, Field
from kindgi import CheckResult, RunTrace, guardrail
class Config(BaseModel): max_chars: int = Field(500, alias="maxChars", gt=0)
@guardrail( id="my-pack.answer-length", name="Answer is short", on_violation="halt", severity="error", config={"maxChars": 60},)def answer_length(config: Config, trace: RunTrace) -> CheckResult: length = len(trace.output or "") if length > config.max_chars: return CheckResult( passed=False, reason=f"The answer is {length} characters; the limit is {config.max_chars}.", ) return CheckResult(passed=True)@guardrail declares both parts: the function is the check, (config, trace),
def or async def; the decorator's arguments are the declaration. The
first parameter's type is the config's schema.
- The id is
<pack-id>.<name>. Name the rule as what should hold:answer-length,response-not-empty. kind: 'zero-llm': the check is your code, a function of the trace. It can't call a model.- The reason is what the violation reports. Say what was wrong.
- The trace has the turn's final answer (
output), its tool calls (toolCalls/tool_calls, each with the tool's name and arguments), their results, its model calls, the user's input and its cost. SeeRunTracein the Python reference.
Put it on an agent
Section titled “Put it on an agent”An agent lists its guardrails:
// in agents/echo-agent/index.tsguardrails: ['my-pack.response-not-empty' as GuardrailId, 'my-pack.answer-length' as GuardrailId],The agent names the guardrail's id, not the check's.
# in agents/echo_agent.pyfrom ..guardrails.answer_length import answer_lengthfrom ..guardrails.response_not_empty import response_not_empty
# … guardrails=[response_not_empty, answer_length],Save. kindgi dev registers the guardrail with the rest of the pack. An
agent that names a guardrail that isn't registered doesn't run:
Error [invalid-request]: Agent "my-pack.gated" references guardrails not in the registry: my-pack.no-such-guardrailRun it
Section titled “Run it”dev-echo answers with the tool's result, which is longer than 60
characters, so the turn stops:
pnpm exec kindgi runs start --agent=my-pack.echo-agent --input='{"userMessage":"hi"}'Error [guardrail-violation]: Turn blocked by guardrail 'my-pack.answer-length': The answer is 86 characters; the limit is 60.npx --yes @kindgi/cli@0.1 runs start --agent=my-pack.echo-agent --input='{"userMessage":"Ada"}'Error [guardrail-violation]: Turn blocked by guardrail 'my-pack.answer-length': The answer is 95 characters; the limit is 60.The command exits with status 1. The run's status is failed, and it has
no answer.
Stop a turn or record a violation shows what the run
records, and how to let the turn finish instead.
A check that throws fails the turn whatever its action, with the exception's
message (handler-throw: Check "…" evaluate() threw: …). Return
passed: false for a rule that isn't met; throw only when the check itself
can't run.
Test the check
Section titled “Test the check”import type { RunId, TenantId } from '@kindgi/sdk/types';import { expect, test } from 'vitest';
import { check } from './index.js';
const trace = (output: string) => ({ runId: 'run-1' as RunId, tenantId: 'test-tenant' as TenantId, output, toolCalls: [], toolResults: [], modelCalls: [], mode: 'runtime' as const,});
test('passes a short answer', async () => { expect(await check.evaluate({ maxChars: 60 }, trace('Shipped.'), {})).toEqual({ passed: true });});
test('fails a long answer, with a reason', async () => { const result = await check.evaluate({ maxChars: 5 }, trace('Shipped yesterday.'), {}); expect(result.passed).toBe(false); expect(result.reason).toBe('The answer is 18 characters; the limit is 5.');});pnpm exec kindgi test ✓ guardrails/answer-length/index.test.ts (2 tests) 2ms… Tests 7 passed (7)from kindgi import RunTracefrom guardrails.answer_length import Config, answer_length
def test_a_short_answer_passes(): trace = RunTrace(run_id="r", tenant_id="t", output="Shipped.") assert answer_length(Config(maxChars=60), trace).passed
def test_a_long_answer_fails_with_a_reason(): trace = RunTrace(run_id="r", tenant_id="t", output="Shipped yesterday.") result = answer_length(Config(maxChars=5), trace) assert not result.passed assert result.reason == "The answer is 18 characters; the limit is 5."uv run pytest........ [100%]8 passed in 0.19s