Using Valibot Schemas for Validating and Typing Agent Responses in Flue
Flue's SDK uses Valibot to provide runtime-validated, strongly-typed outputs from LLM agents by converting schemas into JSON Schema tool definitions and parsing the model's response through valibot.safeParse before returning it to your code.
The withastro/flue repository provides a framework for building LLM agents that require structured, machine-readable outputs. By integrating Valibot schemas, Flue ensures that agent responses conform to expected shapes at both compile-time and runtime, preventing malformed data from propagating through your application while giving the LLM explicit tooling to signal completion.
How Flue Validates Agent Responses with Valibot
Flue implements a layered validation strategy that bridges the gap between LLM-generated text and TypeScript types. According to the source code in packages/sdk/src/result.ts, the validation flow follows six distinct stages:
- Schema Definition: You define a Valibot schema (e.g.,
v.object({ answer: v.number() })) describing the exact shape of data the agent must return. - Tool Generation: When calling
session.prompt(..., { schema }), the SDK invokesbuildResultFooterto generate JSON Schema definitions via@valibot/to-json-schema. - Internal Tool Creation: The SDK creates two internal tools—
finishandgive_up—whose parameter definitions derive from your Valibot schema. - Platform Validation: Before the LLM can invoke
finish, the platform validates arguments against the JSON Schema representation. - Runtime Parsing: Inside
finish, the SDK executesvalibot.safeParse(schema, candidate)at lines 170-173 ofresult.tsto enforce Valibot-specific refinements like coercion and custom checks. - Typed Return: The first successful
finishcall terminates the turn, andResultToolBundle.getOutcome()returns the parsed value with full TypeScript inference viav.InferOutput<S>.
If parsing fails, Flue throws an informative error that the LLM receives as a tool-error, enabling automatic retry with corrected output.
Implementing Valibot Schemas in Your Agents
Basic Schema Definition and Prompt Usage
The simplest way to validate responses is passing a schema directly to session.prompt(). In examples/hello-world/.flue/agents/hello.ts, the SDK demonstrates this pattern:
// examples/hello-world/.flue/agents/hello.ts
import * as v from 'valibot';
export default async function ({ init, log }: FlueContext) {
const { session } = init();
// Define the expected shape of the LLM's answer.
const AnswerSchema = v.object({ answer: v.number() });
// Prompt the model; the SDK injects `finish`/`give_up` tools based on the schema.
const result = await session.prompt(
'What is 2 + 2? Return the answer as a JSON object.',
{ schema: AnswerSchema },
);
// `result` is already typed as `{ answer: number }`
log.info('Validated result', result);
}
The AnswerSchema variable uses v.object() to enforce that the response contains a numeric answer property. The SDK automatically handles the underlying tool registration and validation, returning a value typed according to your schema definition.
Advanced Tool Registration with createResultTools
For scenarios requiring manual tool control, Flue exposes the createResultTools function from packages/sdk/src/result.ts. This approach lets you separate tool registration from outcome retrieval:
import * as v from 'valibot';
import { createResultTools } from '@flue/sdk/result';
const GreetingSchema = v.object({ greeting: v.string() });
const { tools, getOutcome } = createResultTools(GreetingSchema);
// Register the tools with the session (the SDK does this automatically when you pass a schema)
await session.registerTools(tools);
// Later, after the LLM calls `finish`, retrieve the typed value:
const outcome = getOutcome();
if (outcome.type === 'finished') {
// outcome.value is { greeting: string }
console.log('LLM said:', outcome.value.greeting);
}
At lines 129-138 in result.ts, createResultTools converts your Valibot schema to JSON Schema using toJsonSchema and determines whether the top-level type requires an envelope wrapper via needsEnvelope. The getOutcome() method provides access to the strongly-typed result after the LLM completes its task.
Configuration File Validation
Flue extends Valibot validation beyond agent responses to configuration files. The SDK validates flue.config.* files using UserFlueConfigSchema, parsing them with v.safeParse at lines 254-255 of packages/sdk/src/config.ts:
// flue.config.ts (project root)
import { defineConfig } from '@flue/sdk/config';
export default defineConfig({
target: 'node',
// The SDK validates this file against UserFlueConfigSchema (valibot)
});
The configuration is normalized into the internal FlueConfig type after validation, ensuring that project settings match expected schemas before the framework initializes.
Summary
- Flue uses Valibot schemas to define the exact shape of data LLM agents must return, providing static typing through
v.InferOutput<S>and runtime safety viav.safeParse. - The SDK generates internal
finishandgive_uptools from your schema using@valibot/to-json-schema, creating an explicit contract that the LLM must fulfill. - Validation occurs in two stages: first at the platform level against JSON Schema, then at the SDK level using Valibot's parser to handle refinements and custom validations.
- Configuration files are also validated using Valibot schemas in
packages/sdk/src/config.ts, ensuring type safety across the entire project lifecycle. - Failed validations return tool-errors to the LLM, enabling automatic retry loops while keeping your application code free of malformed data.
Frequently Asked Questions
How does Flue handle schema violations in agent responses?
When an LLM provides invalid data, the finish tool implementation in packages/sdk/src/result.ts runs valibot.safeParse and throws an error if validation fails. This error is transmitted back to the LLM as a tool-result error message, allowing the model to retry with corrected parameters. Your application code only receives data after successful parsing, guaranteeing type safety.
Can I use Valibot's advanced features like refinements and transformations?
Yes. Because Flue runs the raw LLM output through v.safeParse at lines 170-173 of result.ts, you can use Valibot's full API including .check(), .transform(), and .brand() modifiers. The SDK validates the data after the LLM supplies it but before returning it to your code, catching schema violations that JSON Schema alone cannot detect, such as custom business logic constraints.
What is the difference between the finish and give_up tools?
Both tools are generated automatically when you provide a schema to session.prompt(). The finish tool validates and returns your schema's output type upon successful completion, while give_up allows the LLM to terminate the session when it cannot fulfill the request. Both tools terminate the current turn, but only finish returns the parsed, typed data through ResultToolBundle.getOutcome().
Where does the JSON Schema conversion happen in the Flue source code?
The conversion from Valibot to JSON Schema occurs in the createResultTools function within packages/sdk/src/result.ts (lines 130-138). This function uses @valibot/to-json-schema to generate the schema representation needed for the finish tool's parameter definition, bridging Valibot's TypeScript-first API with the JSON Schema requirements of the underlying LLM platform.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →