Tool Guardrails - Protecting Your Function Calls
Tool guardrails provide validation and control mechanisms for tool (function) invocations in AI applications. Unlike LLM guardrails which validate prompts and responses from the LLM, tool guardrails operate on the input parameters and output results of tools that the LLM can invoke using function calling.
|
Tool guardrails are important for:
|
Overview
Tool guardrails operate at two stages:
-
Input Guardrails (
@ToolInputGuardrails) - Execute before the tool, validating parameters and context -
Output Guardrails (
@ToolOutputGuardrails) - Execute after the tool, validating or transforming results
Both types can:
-
Allow execution to proceed (
success()) -
Block execution with an error message (
failure()) -
Transform requests/responses (
successWith())
Quick Start
To create your first guardrail in 3 steps:
-
Create a CDI bean implementing
ToolInputGuardrailorToolOutputGuardrail -
Implement the
validate()method with your validation logic -
Apply the annotation to your tool method
// Step 1 & 2: Create guardrail bean
@ApplicationScoped (1)
public class PositiveNumberGuardrail implements ToolInputGuardrail { (2)
@Override
public ToolInputGuardrailResult validate(ToolInputGuardrailRequest request) { (3)
JsonObject args = request.argumentsAsJson();
int number = args.getInteger("amount");
if (number < 0) {
return ToolInputGuardrailResult.failure( (4)
"Amount must be positive, got: " + number);
}
return ToolInputGuardrailResult.success(); (5)
}
}
// Step 3: Apply to tool
@ApplicationScoped
public class BankingTools {
@Tool("Transfer money between accounts")
@ToolInputGuardrails({ PositiveNumberGuardrail.class }) (6)
public String transfer(int amount, String from, String to) {
// This only executes if amount is positive
return "Transferred $" + amount + " from " + from + " to " + to;
}
}
| 1 | Make it a CDI bean with a scope annotation |
| 2 | Implement the appropriate guardrail interface |
| 3 | Override the validate method |
| 4 | Return failure if validation fails |
| 5 | Return success if validation passes |
| 6 | Apply the guardrail annotation to your tool |
Basic Usage
Input Guardrails
Input guardrails validate tool parameters before execution:
@ApplicationScoped
public class EmailTools {
@Tool("Send an email to a recipient")
@ToolInputGuardrails({ EmailFormatValidator.class })
public String sendEmail(String to,
String subject,
String body) {
// This only executes if EmailFormatValidator passes
return "Email sent to " + to;
}
}
The guardrail implementation:
@ApplicationScoped
public class EmailFormatValidator implements ToolInputGuardrail {
private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$");
@Override
public ToolInputGuardrailResult validate(ToolInputGuardrailRequest request) {
// Parse arguments as JsonObject for easy access
JsonObject args = request.argumentsAsJson();
String email = args.getString("to");
if (!EMAIL_PATTERN.matcher(email).matches()) {
return ToolInputGuardrailResult.failure(
"Invalid email format: " + email);
}
return ToolInputGuardrailResult.success();
}
}
|
When an input guardrail fails:
|
Output Guardrails
Output guardrails validate and transform tool results:
@Tool("Get customer information")
@ToolOutputGuardrails({ SensitiveDataFilter.class })
public String getCustomerInfo(String customerId) {
// Returns customer data including SSN, credit cards, etc.
return customerDatabase.findById(customerId).toString();
}
The guardrail implementation:
@ApplicationScoped
public class SensitiveDataFilter implements ToolOutputGuardrail {
private static final Pattern SSN_PATTERN =
Pattern.compile("\\b\\d{3}-\\d{2}-\\d{4}\\b");
@Override
public ToolOutputGuardrailResult validate(ToolOutputGuardrailRequest request) {
String result = request.resultText();
// Filter sensitive data
String filtered = SSN_PATTERN.matcher(result)
.replaceAll("[REDACTED-SSN]");
if (!filtered.equals(result)) {
// Return modified result
return ToolOutputGuardrailResult.successWith(
ToolExecutionResult.builder()
.toolName(request.toolName())
.resultText(filtered)
.build()
);
}
return ToolOutputGuardrailResult.success();
}
}
|
When an output guardrail modifies the result:
|
Guardrail Interfaces
ToolInputGuardrail Interface
public interface ToolInputGuardrail {
/**
* Validates tool input parameters before execution.
*/
ToolInputGuardrailResult validate(ToolInputGuardrailRequest request);
}
Request Context:
// Access available context
String toolName = request.toolName();
String arguments = request.arguments(); // JSON string
JsonObject args = request.argumentsAsJson(); // Parsed JSON for easy access
Object memoryId = request.memoryId();
ToolMetadata metadata = request.toolMetadata();
ToolInvocationContext context = request.invocationContext();
Result Options:
// Validation passed
return ToolInputGuardrailResult.success();
// Validation passed with modified request
return ToolInputGuardrailResult.successWith(modifiedRequest);
// Validation failed - error returned to LLM
return ToolInputGuardrailResult.failure("Error message for LLM");
// Fatal failure - stops execution immediately
return ToolInputGuardrailResult.fatal("Critical error", exception);
// Fatal failure - uses exception message
return ToolInputGuardrailResult.fatal(exception);
ToolOutputGuardrail Interface
public interface ToolOutputGuardrail {
/**
* Validates tool output after execution.
*/
ToolOutputGuardrailResult validate(ToolOutputGuardrailRequest request);
}
Request Context:
// Access tool result and context
String resultText = request.resultText();
JsonObject result = request.resultAsJson(); // Parsed result for JSON tools
boolean isError = request.isError();
String toolName = request.toolName();
JsonObject args = request.argumentsAsJson(); // Original input arguments
ToolExecutionRequest originalRequest = request.executionRequest();
ToolExecutionResult executionResult = request.executionResult();
Result Options:
// Output valid
return ToolOutputGuardrailResult.success();
// Output valid with transformation
return ToolOutputGuardrailResult.successWith(modifiedResult);
// Output invalid - error returned to LLM
return ToolOutputGuardrailResult.failure("Error message");
// Fatal failure - stops execution immediately
return ToolOutputGuardrailResult.fatal("Critical error", exception);
// Fatal failure - uses exception message
return ToolOutputGuardrailResult.fatal(exception);
Multiple Guardrails
Multiple guardrails can be applied to a single tool and execute them in order:
@Tool("Send bulk emails")
@ToolInputGuardrails({
EmailFormatValidator.class, // Executes first
UserAuthorizationGuardrail.class, // Executes second
RateLimitGuardrail.class // Executes third
})
@ToolOutputGuardrails({
OutputSizeLimiter.class, // Executes first
SensitiveDataFilter.class // Executes second
})
public String sendBulkEmail(String recipients, String subject, String body) {
// Implementation
}
|
Execution Order Best Practices:
|
|
Fail-Fast Behavior: Input guardrails execute in order and stop at the first failure. If |
Working with Parameters
Accessing JSON Parameters
Tool arguments are provided as JSON strings. Use argumentsAsJson() for easy parameter access:
import io.vertx.core.json.JsonObject; (1)
import io.vertx.core.json.JsonArray;
@Override
public ToolInputGuardrailResult validate(ToolInputGuardrailRequest request) {
JsonObject args = request.argumentsAsJson(); (2)
// Type-safe parameter access
int age = args.getInteger("age");
String name = args.getString("name");
boolean active = args.getBoolean("active", false); // with default
// Check for parameter existence
if (args.containsKey("email")) {
String email = args.getString("email");
}
// Nested objects
JsonObject address = args.getJsonObject("address");
String city = address.getString("city");
// Arrays
JsonArray tags = args.getJsonArray("tags");
return ToolInputGuardrailResult.success();
}
| 1 | Import Vert.x JSON classes (included with Quarkus) |
| 2 | Parse arguments to JsonObject for easy access |
|
|
POJO Mapping
For complex parameters, map JSON directly to Java objects:
// Define parameter structure as a record
record EmailParams(
String to,
String subject,
String body,
List<String> attachments
) {}
@Override
public ToolInputGuardrailResult validate(ToolInputGuardrailRequest request) {
// Map arguments to POJO
EmailParams params = request.argumentsAsJson().mapTo(EmailParams.class);
// Access strongly-typed parameters
if (params.to() == null || params.to().isEmpty()) {
return ToolInputGuardrailResult.failure("Email recipient is required");
}
if (params.attachments().size() > 5) {
return ToolInputGuardrailResult.failure("Maximum 5 attachments allowed");
}
return ToolInputGuardrailResult.success();
}
Parsing Tool Output
Output guardrails can also parse JSON results:
record CustomerData(
String id,
String name,
String ssn,
String creditCard
) {}
@Override
public ToolOutputGuardrailResult validate(ToolOutputGuardrailRequest request) {
// Parse JSON output
CustomerData customer = request.resultAsJson().mapTo(CustomerData.class);
// Filter sensitive fields
CustomerData filtered = new CustomerData(
customer.id(),
customer.name(),
"[REDACTED]",
"[REDACTED]"
);
// Return filtered result as JSON
JsonObject filteredJson = JsonObject.mapFrom(filtered);
return ToolOutputGuardrailResult.successWith(
ToolExecutionResult.builder()
.toolName(request.toolName())
.resultText(filteredJson.encode())
.build()
);
}
Common Use Cases
Security and Authorization
@RequestScoped
public class UserAuthorizationGuardrail implements ToolInputGuardrail {
@Inject
SecurityIdentity securityIdentity;
@Override
public ToolInputGuardrailResult validate(ToolInputGuardrailRequest request) {
if (securityIdentity.isAnonymous()) {
return ToolInputGuardrailResult.failure(
"Authentication required to execute " + request.toolName());
}
if (!securityIdentity.hasRole("ADMIN")) {
return ToolInputGuardrailResult.failure(
"Insufficient permissions to execute " + request.toolName());
}
return ToolInputGuardrailResult.success();
}
}
|
Guardrails are CDI beans and can inject dependencies like |
Rate Limiting
@ApplicationScoped
public class RateLimitGuardrail implements ToolInputGuardrail {
@Inject
RateLimiterService rateLimiter;
@Override
public ToolInputGuardrailResult validate(ToolInputGuardrailRequest request) {
String userId = getUserId(request);
if (!rateLimiter.tryAcquire(userId, request.toolName())) {
return ToolInputGuardrailResult.failure(
"Rate limit exceeded. Please try again later.");
}
return ToolInputGuardrailResult.success();
}
}
Data Privacy and Filtering
@ApplicationScoped
public class PiiFilter implements ToolOutputGuardrail {
private static final List<Pattern> PII_PATTERNS = List.of(
Pattern.compile("\\b\\d{3}-\\d{2}-\\d{4}\\b"), // SSN
Pattern.compile("\\b\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4}\\b") // Credit card
);
@Override
public ToolOutputGuardrailResult validate(ToolOutputGuardrailRequest request) {
String result = request.resultText();
String filtered = result;
for (Pattern pattern : PII_PATTERNS) {
filtered = pattern.matcher(filtered).replaceAll("[REDACTED]");
}
if (!filtered.equals(result)) {
return ToolOutputGuardrailResult.successWith(
ToolExecutionResult.builder()
.toolName(request.toolName())
.resultText(filtered)
.build()
);
}
return ToolOutputGuardrailResult.success();
}
}
Cost Control (Output Size Limiting)
@ApplicationScoped
public class OutputSizeLimiter implements ToolOutputGuardrail {
private static final int MAX_CHARACTERS = 4000;
@Override
public ToolOutputGuardrailResult validate(ToolOutputGuardrailRequest request) {
String result = request.resultText();
if (result.length() > MAX_CHARACTERS) {
String truncated = result.substring(0, MAX_CHARACTERS) +
"\n\n[Output truncated. Please refine your query.]";
return ToolOutputGuardrailResult.successWith(
ToolExecutionResult.builder()
.toolName(request.toolName())
.resultText(truncated)
.build()
);
}
return ToolOutputGuardrailResult.success();
}
}
CDI Integration
Guardrails are CDI beans and support all standard features:
@RequestScoped // Can use different scopes
public class StatefulGuardrail implements ToolInputGuardrail {
@Inject
SecurityIdentity identity; // Dependency injection
@Inject
AuditLogger auditLogger; // Custom beans
@ConfigProperty(name = "app.max-retries", defaultValue = "3")
int maxRetries; // Configuration injection
private int callCount = 0; // Instance state
@Override
public ToolInputGuardrailResult validate(ToolInputGuardrailRequest request) {
callCount++;
auditLogger.log(identity.getPrincipal().getName(),
"Tool invocation #" + callCount);
// Validation logic
return ToolInputGuardrailResult.success();
}
}
Advanced Topics
Important Limitation: Tool Guardrails and Event Loop Compatibility
Tool guardrails (both @ToolInputGuardrails and @ToolOutputGuardrails) have synchronous, blocking APIs by design, as
they perform operations like JSON parsing, validation, and potentially database lookups or external API calls.
Consequently, tools annotated with guardrails cannot execute on the Vert.x event loop and must be marked as blocking. If you attempt to use a tool with guardrails from the event loop (for example, in a reactive endpoint or streaming AI service), the framework will throw a ToolExecutionException with the message "Cannot execute guardrails on the event loop thread."
To use guardrails with your tools, ensure the tool methods are properly detected or annotated as blocking (e.g., using @Blocking annotation or by returning blocking types instead of Uni/Multi).
Tools without guardrails can continue to use reactive execution models without any restrictions. This architectural decision ensures application responsiveness by preventing blocking operations on the event loop.
Request Modification
Input guardrails can modify tool requests before execution:
@ApplicationScoped
public class ParameterNormalizer implements ToolInputGuardrail {
@Override
public ToolInputGuardrailResult validate(ToolInputGuardrailRequest request) {
// Parse arguments as JsonObject
JsonObject args = request.argumentsAsJson();
// Normalize email to lowercase
String email = args.getString("email").toLowerCase();
args.put("email", email);
// Build modified request with normalized parameters
ToolExecutionRequest modified = ToolExecutionRequest.builder()
.id(request.executionRequest().id())
.name(request.executionRequest().name())
.arguments(args.encode()) // Convert back to JSON string
.build();
return ToolInputGuardrailResult.successWith(modified);
}
}
Context and Metadata
Guardrails have access to contextual information:
@Override
public ToolInputGuardrailResult validate(ToolInputGuardrailRequest request) {
// Tool information
ToolMetadata metadata = request.toolMetadata();
String toolName = metadata.toolName();
String description = metadata.description();
// Invocation context
ToolInvocationContext context = request.invocationContext();
Object memoryId = context.memoryId();
Map<String, Object> params = context.parameters();
// Use context for validation...
return ToolInputGuardrailResult.success();
}
Error Handling
Guardrails should handle parsing and validation errors gracefully:
@Override
public ToolInputGuardrailResult validate(ToolInputGuardrailRequest request) {
try {
JsonObject args = request.argumentsAsJson();
int value = args.getInteger("amount");
if (value < 0) {
return ToolInputGuardrailResult.failure(
"Amount must be positive, got: " + value);
}
return ToolInputGuardrailResult.success();
} catch (DecodeException e) {
// JSON parsing failed
return ToolInputGuardrailResult.failure(
"Invalid JSON arguments: " + e.getMessage());
} catch (ClassCastException e) {
// Type mismatch (e.g., string instead of integer)
return ToolInputGuardrailResult.failure(
"Invalid parameter type: " + e.getMessage());
} catch (Exception e) {
// Unexpected error - use fatal failure to stop execution
return ToolInputGuardrailResult.fatal(
"Validation error: " + e.getMessage(), e);
}
}
|
Fatal vs Non-Fatal Failures:
Use fatal failures only for critical errors that indicate bugs or system failures (e.g., authorization failures, system errors), not for validation errors that the LLM can potentially correct. |
Best Practices
Follow these guidelines when implementing guardrails:
@ApplicationScoped
public class WellDesignedGuardrail implements ToolInputGuardrail {
@Override
public ToolInputGuardrailResult validate(ToolInputGuardrailRequest request) {
try {
// 1. Use argumentsAsJson() for easy parameter access
JsonObject args = request.argumentsAsJson();
// 2. Provide clear, actionable error messages for the LLM
int age = args.getInteger("age", -1);
if (age < 0) {
return ToolInputGuardrailResult.failure(
"Parameter 'age' must be a non-negative integer. Got: " + age
);
}
// 3. Check required parameters exist
if (!args.containsKey("email")) {
return ToolInputGuardrailResult.failure(
"Missing required parameter: 'email'"
);
}
// 4. Validate formats with helpful messages
String email = args.getString("email");
if (!isValidEmail(email)) {
return ToolInputGuardrailResult.failure(
"Invalid email format: '" + email + "'. " +
"Expected format: user@example.com"
);
}
return ToolInputGuardrailResult.success();
} catch (DecodeException e) {
// 5. Handle JSON parsing errors gracefully
return ToolInputGuardrailResult.failure(
"Arguments must be valid JSON. " + e.getMessage()
);
}
}
}
Testing
Test guardrails as regular CDI beans:
@QuarkusTest
public class EmailValidatorTest {
@Inject
EmailFormatValidator validator;
@Test
public void testValidEmail() {
ToolExecutionRequest request = ToolExecutionRequest.builder()
.id("1")
.name("sendEmail")
.arguments("{\"to\":\"user@example.com\"}")
.build();
ToolInputGuardrailRequest guardrailRequest =
new ToolInputGuardrailRequest(request, null, null);
ToolInputGuardrailResult result = validator.validate(guardrailRequest);
assertThat(result.isSuccess()).isTrue();
}
@Test
public void testInvalidEmail() {
ToolExecutionRequest request = ToolExecutionRequest.builder()
.id("1")
.name("sendEmail")
.arguments("{\"to\":\"invalid-email\"}")
.build();
ToolInputGuardrailRequest guardrailRequest =
new ToolInputGuardrailRequest(request, null, null);
ToolInputGuardrailResult result = validator.validate(guardrailRequest);
assertThat(result.isSuccess()).isFalse();
assertThat(result.errorMessage()).contains("Invalid email");
}
}
Observability and Metrics
Tool guardrail execution can be monitored using Micrometer metrics when the quarkus-micrometer extension is present.
Metrics
Two metrics are automatically recorded for each guardrail execution:
Counter Metric: tool-guardrail.invoked
Counts the number of times a tool guardrail is invoked, with tags:
-
aiservice- Fully qualified AI service interface name -
operation- AI service method name -
tool.name- Name of the tool being validated -
guardrail- Guardrail class name -
guardrail.type-inputoroutput -
outcome-success,failure, orfatal
Timer Metric: tool-guardrail.timed
Records execution time of guardrails with the same tags as the counter, plus percentiles (75th, 95th, 99th).
Events
For custom observability, you can observe CDI events fired during guardrail execution:
@ApplicationScoped
public class GuardrailMonitor {
public void onInputGuardrailExecuted(
@Observes ToolInputGuardrailExecutedEvent event) {
String toolName = event.toolName();
Class<?> guardrailClass = event.toolClass();
ToolGuardrailOutcome outcome = event.outcome(); // SUCCESS, FAILURE, FATAL
long durationNanos = event.duration();
// Custom monitoring logic
if (outcome == ToolGuardrailOutcome.FATAL) {
logger.error("Fatal guardrail failure for tool: {}", toolName);
}
}
public void onOutputGuardrailExecuted(
@Observes ToolOutputGuardrailExecutedEvent event) {
// Similar monitoring for output guardrails
}
}
These events provide access to:
-
toolName()- The tool being validated -
toolClass()- The guardrail class -
outcome()- SUCCESS, FAILURE, or FATAL -
duration()- Execution time in nanoseconds -
toolInvocationContext()- Full invocation context including memory ID and parameters
|
Use these metrics to:
|
Going Further
-
Function Calling – Learn how to declare and use tools in AI services
-
LLM Guardrails – Understand guardrails for prompts and responses
-
AI Services Reference – Complete AI service configuration guide