
Most AI content focuses on building something that works once. Production systems require something else entirely: predictable behavior under failure. If you have built agents, RAG pipelines, or tool-driven AI workflows, you have already seen the gap. The project works, then real inputs arrive. Tools fail, timeouts occur, and the model produces output that sounds confident but is structurally wrong.
In this issue, we build a minimal but production-aligned guardrailed AI assistant in C# that runs fully locally using Ollama. The design focuses on clear boundaries, strict contracts, deterministic execution, and safe failure modes. The system is intentionally small. It is not autonomous and not agentic. It is a reference architecture for adding guardrails to any .NET AI workflow.
Why Guardrails Matter
LLMs are probabilistic components. They produce untrusted output. Tool invocation multiplies risk because the output of the model becomes input to deterministic systems. Without constraints, a single malformed response can cascade into runtime failures, incorrect actions, or silent corruption.
A production architecture needs to answer these questions:
- What is allowed to reach the model
- What is allowed to reach tools
- What happens when the model output is invalid
- What happens when tools are slow, unavailable, or inconsistent
- What the system should do when it cannot be confident
Guardrails are not a prompt. They are infrastructure.
System Overview
This project implements a guardrailed execution pipeline with explicit separation between probabilistic reasoning and deterministic execution.
The system uses two local Ollama models:
- A chat model (llama3.2:3b) for planning and response generation
- An embedding model (nomic-embed-text:latest) for semantic retrieval
The core layers are:
- Input validation guardrails
- Deterministic policy guardrails
- Tool planning through a strict JSON contract
- Tool execution via allowlists, argument validation, and timeouts
- Grounded output generation with safe fallbacks
The runtime flow is:
User input → deterministic checks → tool plan → validated tool execution → grounded final response
The assistant never calls tools directly. It must first produce a ToolPlan, which is validated and executed through a constrained registry.
Architecture Diagram
The guardrailed execution pipeline follows a layered architecture that maps directly to the code:
Input Layer validates input and applies deterministic policies before any model call.
Planning Layer uses the chat model to produce a strict JSON ToolPlan with a limited action set.
Execution Layer runs only allowlisted tools with argument validation and timeouts, including semantic search when required.
Output Layer generates the final response using tool output only, falling back safely when information is insufficient.
The following flowchart illustrates the system architecture:
This design isolates probabilistic reasoning from deterministic execution and ensures predictable, safe failure modes at every stage.
1. Input validation guardrails
Before any model call, the system validates the input.
This includes:
- Empty input detection
- Maximum length constraints
- Basic prompt injection and exfiltration pattern checks
var validation = InputValidator.ValidateUserInput(input, config.MaxInputChars);
if (!validation.Ok)
{
Console.WriteLine($"Assistant: {validation.ErrorMessage}");
continue;
}This prevents unnecessary model calls and ensures obvious malicious patterns are rejected deterministically.
2. Deterministic policy guardrails
Not everything should be delegated to an LLM. Some categories must be handled deterministically.
This project enforces two boundaries:
Security sensitive requests
Requests involving credentials, secrets, hacking, bypassing, or exploitation are refused deterministically.
if (ContainsSecretRequest(s))
return "I can’t help with credential, secret, or hacking-related requests.";Simple computation
Math expressions are computed locally using a deterministic parser. No model call occurs.
var math = TryComputeMath(s);
if (math is not null)
return math;This highlights a key principle. If correctness is required, the system should not be probabilistic.
3. Tool planning through a strict JSON contract
Tool use is the most common source of failure in production AI systems. The model can hallucinate tool names, invent arguments, or return output that cannot be parsed safely.
Instead of letting the model call tools directly, we require a strict JSON ToolPlan:
{
"action": "tool",
"toolName": "Runbooks.Search",
"arguments": { "query": "redis incident troubleshooting" },
"answer": null
}The planner is a constrained planning component. It only returns one of three actions:
- tool
- answer
- refuse
The system message specifies allowed tools and refusal rules. This prevents tool sprawl and improves predictability.
var plan = await planner.PlanAsync(input);
if (plan.Action == ToolPlanAction.Refuse)
{
Console.WriteLine("Assistant: I can’t help with that request.");
continue;
}4. Tool plan validation and lifecycle safety
Even if the model returns JSON, that does not mean it is safe to use.
ToolPlanner validates:
- JSON format
- Required properties
- Allowed action values
- Tool name presence when action is tool
A subtle production issue is JSON lifetime. JsonElement references the backing JsonDocument. If the document is disposed, accessing the element later throws ObjectDisposedException.
The fix is to clone arguments before returning them:
JsonElement? args = null;
if (root.TryGetProperty("arguments", out var argProp) && argProp.ValueKind != JsonValueKind.Null)
args = argProp.Clone();This is a concrete example of where guardrails include runtime memory safety, not just prompt discipline.
5. Tool execution guardrails
Tools execute through a ToolRegistry that enforces:
- Tool allowlisting
- Argument validation per tool
- Timeouts through cancellation tokens
- Safe failure messages
Only tools in the allowlist may run:
private static readonly HashSet<string> AllowedTools = new(StringComparer.OrdinalIgnoreCase)
{
"WorldTime.GetCityTime",
"Runbooks.Search"
};Tool arguments are validated before execution:
if (!args.Value.TryGetProperty("query", out var queryProp))
return ToolExecutionResult.Fail("Missing required argument: query");Timeouts are enforced:
using var cts = new CancellationTokenSource(_config.ToolTimeout);If embeddings are slow or the model is cold starting, timeouts will trigger. This is not an error to hide. It is a failure mode to design for.
6. Output guardrails through grounded response generation
The final response is generated from tool output only. This prevents the model from filling gaps with plausible text.
The final answer stage enforces:
- Use only TOOL_OUTPUT content
- Return "I don’t know." when tool output is insufficient
- Short response constraint for readability
var final = await planner.FinalAnswerAsync(
userInput: input,
toolName: plan.ToolName!,
toolOutput: toolResult.Output!);This is one of the highest leverage guardrails in production systems because it converts hallucination into a controlled failure.
Example Workflow
User input:
"I want you to tell me how I should fix Redis incidents"
Expected system behavior:
- Input passes validation
- Planner selects Runbooks.Search
- Tool registry executes semantic search over internal documents
- Final answer is generated only from retrieved runbook excerpts
The user gets a grounded response. If the system cannot retrieve relevant content, it responds with "I don’t know." rather than inventing.
Key Advantages
This architecture provides:
- Clear separation between probabilistic reasoning and deterministic execution
- Explicit tool contracts and validation points
- Predictable failure modes and safe fallback behavior
- A local first workflow with no cloud dependency
- A foundation that scales to real production systems
Potential Enhancements
This reference implementation can be extended incrementally:
- Per tool timeouts and retry policies (Polly)
- Structured logging and tracing (OpenTelemetry)
- Metrics on refusal rate, timeout rate, and tool usage
- Stronger schema validation using generated DTOs or JSON Schema
- Deterministic intent routing before tool planning for known intents
- Persistent vector storage using pgvector or Qdrant
None of these require changing the core concept. They build on the same guardrailed execution pipeline.
Final Notes
Reliable AI systems are built by constraining uncertainty, not by amplifying it. The practical path to production is not more autonomy. It is stronger boundaries, explicit contracts, and safe failure modes.
This issue demonstrates how those principles translate directly into code.
Explore the source code at the GitHub repository.
See you in the next issue.
Stay curious.
Join the Newsletter
Subscribe for AI engineering insights, system design strategies, and workflow tips.