Per-Tool Guardrails (TypeScript)

Guard a single tool's arguments and results without hand-wrapping the function

Guard a single tool’s arguments and results without hand-wrapping the function — declare inputGuardrails / outputGuardrails right on the tool, next to approval and restartSafe.

typescript
import { Agent, tool } from 'praisonai';

const sendEmail = tool({
  name: 'send_email',
  description: 'Send an email',
  inputGuardrails: [(args) =>
    String(args.to ?? '').endsWith('@corp.com')
      ? [true, args]
      : [false, 'Recipient is outside the company domain.']],
  execute: async ({ to, body }) => `sent to ${to}`,
});

const agent = new Agent({
  instructions: 'Send emails on request.',
  tools: [sendEmail],
});

await agent.start('Email alice@gmail.com the release notes');
// -> the call is blocked; the reason is fed back to the model, which can adapt.

The guardrail fires on every invocation of send_email and never for any other tool.

Diagram

block

allow / rewrite

block

allow / substitute

LLM tool call

input guardrail

reason fed to model

tool runs

output guardrail

reason fed to model

back to LLM

Quick Start

  1. Block a tool's arguments by rule

    An input guardrail returns [false, 'reason'] to block. The reason is handed back to the model as the tool result — never thrown at the user.

    typescript
    import { Agent, tool } from 'praisonai';
    
    const sendEmail = tool({
      name: 'send_email',
      description: 'Send an email',
      inputGuardrails: [(args) =>
        String(args.to ?? '').endsWith('@corp.com')
          ? [true, args]
          : [false, 'Recipient is outside the company domain.']],
      execute: async ({ to, body }) => `sent to ${to}`,
    });
    
    const agent = new Agent({ instructions: 'Send emails.', tools: [sendEmail] });
    await agent.start('Email bob@corp.com the agenda');   // allowed
  2. Rewrite the arguments before the tool runs

    Return [true, newArgs] to rewrite. The rewritten object is what the tool receives.

    typescript
    import { Agent, tool } from 'praisonai';
    
    const sendEmail = tool({
      name: 'send_email',
      description: 'Send an email',
      inputGuardrails: [(args) => [true, { ...args, bcc: 'audit@corp.com' }]],
      execute: async ({ to, body, bcc }) => `sent to ${to}, bcc ${bcc}`,
    });
    
    const agent = new Agent({ instructions: 'Send emails.', tools: [sendEmail] });
    await agent.start('Email dana@corp.com the report');
  3. Substitute the result

    An output guardrail sees the raw result before it re-enters the LLM context. Return [true, newResult] to substitute, or [false, 'reason'] to block.

    typescript
    import { Agent, tool } from 'praisonai';
    
    const readConfig = tool({
      name: 'read_config',
      description: 'Read the service config',
      outputGuardrails: [(result) => [true, String(result).replace('sk-live-1234', '[REDACTED]')]],
      execute: async () => 'STRIPE_KEY=sk-live-1234',
    });
    
    const agent = new Agent({ instructions: 'Report config values.', tools: [readConfig] });
    await agent.start('What is the Stripe key?');
    // -> the model sees STRIPE_KEY=[REDACTED]
  4. Chain several guardrails

    Pass an array — each guardrail’s allowed value feeds the next; the chain short-circuits on the first failure.

    typescript
    import { Agent, tool } from 'praisonai';
    
    const sendEmail = tool({
      name: 'send_email',
      description: 'Send an email',
      inputGuardrails: [
        (args) => String(args.to ?? '').endsWith('@corp.com')
          ? [true, args]
          : [false, 'off-domain recipient'],
        (args) => [true, { ...args, bcc: 'audit@corp.com' }],
      ],
      execute: async (args) => `sent to ${args.to}, bcc ${args.bcc}`,
    });
    
    const agent = new Agent({ instructions: 'Send emails.', tools: [sendEmail] });
    await agent.start('Email carol@corp.com the notes');

Verdict shapes accepted

A guardrail’s return value tells the runtime whether to allow, rewrite, or block.

Return valueMeaning
[true, value]Allow; value replaces args/result (rewrite / substitute).
[true, null] / [true, undefined]Allow unchanged — does NOT replace with null.
[false, 'reason']Block; reason goes back to the model.
true / falseBare boolean — allow unchanged, or block with default reason.
GuardrailValidationResult ({ success, result?, error? })Same rules using the existing shape.
null / undefined“No opinion” — allow unchanged.
Anything elseFails closed — an unreadable verdict is not an approval.
ThrowsFails closed unless the chain’s failOpen is set.

How per-tool guardrails run

Per-tool guardrails are the layer closest to the tool in both directions. The input guardrail runs last on the way in (immediately before dispatch); the output guardrail runs first on the way out (before any agent-wide handling).

Diagram
Trust fencePer-tool output guardrailToolPer-tool input guardrailAgent-wide guardrail / policyHuman approval gateLLMTrust fencePer-tool output guardrailToolPer-tool input guardrailAgent-wide guardrail / policyHuman approval gateLLMruns LAST in — nothing canrewrite args after its verdictruns FIRST out — sees theRAW result before the fenceproposed tool callapproved args(possibly rewritten) argsallowed / rewritten argsraw resultallowed / substituted resultfenced result

Detecting a denial

A blocked call returns a ToolGuardrailDenial object rather than throwing.

typescript
import { isToolGuardrailDenial } from 'praisonai';

const result = await sendEmail.execute({ to: 'alice@gmail.com', body: 'hi' });
if (isToolGuardrailDenial(result)) {
  console.log(result.error);            // "Tool 'send_email' was blocked by an input guardrail: ..."
  console.log(result.tool_guardrail);   // 'input' | 'output'
}

The model receives this same object as the tool result, tagged guardrail_denied: true, so it can pick different arguments, choose another tool, or explain to the user — never a stack trace.

typescript
interface ToolGuardrailDenial {
  error: string;                    // "Tool 'X' was blocked by an input guardrail: <reason>"
  guardrail_denied: true;
  tool_guardrail: 'input' | 'output';
}

Reusing an existing guardrail object

Any object exposing validateToolCall(toolName, args) or validateToolResult(toolName, result) — including an existing agent-wide ToolGuardrailChain — drops in as a single entry.

typescript
import { Agent, tool, ToolInputGuardrail } from 'praisonai';

const domainCheck = new ToolInputGuardrail(
  (args) => String(args.to ?? '').endsWith('@corp.com')
    ? [true, args]
    : [false, 'off-domain'],
  'domain_check',
);

const sendEmail = tool({
  name: 'send_email',
  description: 'Send an email',
  inputGuardrails: [domainCheck],
  execute: async ({ to }) => `sent to ${to}`,
});

const agent = new Agent({ instructions: 'Send emails.', tools: [sendEmail] });
await agent.start('Email erin@corp.com the plan');

Per-tool vs agent-wide guardrails

Per-tool guardrails share the same machinery as agent-wide ones — a guardrail written for one scope drops into the other.

Agent-wide Agent({ guardrails })Per-tool inputGuardrails / outputGuardrails
Fires forEvery tool callJust this tool
Configured onAgent({...})The tool itself, next to approval and restartSafe
Blocks byReturning [false, ...]Same
On blockReason fed back to the modelSame
Rewrite / substitute[true, value]Same

Best Practices

Scope the guardrail to where the rule lives

When a rule belongs to one tool — “send_email recipients must be on-domain” — declare it on that tool, not agent-wide. An agent-wide guardrail fires for every tool and has to branch on toolName.

Prefer rewriting arguments over blocking

Returning [true, sanitisedArgs] is safer than [false, reason] and hoping the model retries correctly. Fix the call rather than bouncing it back.

Redact in the output guardrail rather than reject

When a result carries a secret, return [true, redacted] so the model still gets a usable answer, instead of [false, ...] which throws the whole result away.

Keep guardrail logic pure

A guardrail should inspect and transform its input with no side effects. A guardrail that throws fails closed and blocks the tool, unless the chain’s failOpen is set.


Related