If you've connected an LLM to tool calling, APIs, or database queries recently, you know the feeling: agents are terrifyingly unpredictable.
Giving an LLM the ability to decide its own next step means shifting from passive text generation to active system execution. In demos, it looks like magic: the agent reasons, picks a tool, fetches data, and loops until the task is solved.
In real life?
- An indirect prompt injection slipped inside an ingested PDF instructs your agent to exfiltrate private DB records via an outbound HTTP webhook.
- A looping reasoning process burns 80,000 reasoning tokens in 12 turns on a dead-end premise, costing 50x more than expected.
- Sensitive customer PII (credit cards, emails, internal keys) is handed directly to third-party model providers inside execution context.
The traditional answer to this is the classic LLMOps sledgehammer: spin up Docker containers for Langfuse, Helicone, OpenLit, a vector database, ClickHouse, Redis, and an S3 bucket just to trace calls and apply basic guardrails.
For most developers and lean teams, the monitoring infrastructure ends up costing more in maintenance and compute than the LLM calls themselves.
There is a better way: handling security, guardrails, and cost controls in-process, directly at the application runtime level.
1. The Anatomy of an Agent Failure
When building autonomous workflows or multi-step agents, security vulnerabilities aren't theoretical — they fall into three clear failure modes:
[User / External Input]
│
▼
┌───────────────┐
│ Prompt / │ <-- 1. Prompt Injection / Malicious Context
│ Ingested Data │
└───────┬───────┘
▼
┌───────────────┐
│ LLM Engine │ <-- 2. Token Loop & Budget Drainage (Hallucination)
└───────┬───────┘
▼
┌───────────────┐
│ Tool Execution│ <-- 3. Unauthorized Side-Effects / PII Exfiltration
└───────────────┘
- Unchecked Tool Execution & Side-Effects: Once an agent has tools, an injection is no longer just a jailbreak: it becomes Remote Code Execution (RCE) or arbitrary API manipulation.
- Context Contamination & Data Leaks: If your RAG or tool outputs include raw PII, your LLM will gladly send it over the wire to external APIs or echo it into log sinks.
- Runaway Loops & Cost Explosion: Without per-turn token limits and hard budget circuit-breakers, recursive tool loops can easily drain your monthly quota in minutes.
2. The Solution: Zero-Infra Control Plane with avantGate
Meet AG-Men (avantGate's in-process engine): a tiny, sub-millisecond guardian that sits directly between your model's thoughts and your backend APIs.
Avant Gate runs directly in your TypeScript process.
The Journey of an Intercepted Run
Imagine a user submits an invoice query packed with an indirect prompt injection and a customer's raw credit card. Here is how the AG-Men defend your runtime step by step:
- The Cleaner (Local PII Sanitizer): Before a single byte leaves server memory, The Cleaner scrubs emails and credit cards on the fly. The external LLM never catches a glimpse of the private data.
- The Breaker (Budget Circuit-Breaker): Turn three hits an infinite recursive loop triggered by the injection. The Breaker detects the runaway token surge, trips the fuse, and cuts the execution instantly.
- The Law (Deterministic Guardrails): The hallucinating agent tries to force an unvetted database write. The Law blocks it cold against a strict Zod contract — zero unauthorized side-effects.
-
The Shapeshifter (Resilient Fallback): The primary model throws a sudden
529 Overloaded. The Shapeshifter catches the fall mid-air and swaps to a backup provider, landing the mission safely without a glitch.
3. Hands-on: Securing an Agent Workflow
Here is how you wrap your agent runtime with avantGate in TypeScript:
Step 1: Initialize the Control Plane
import { AvantGate } from 'avantgate';
const gate = new AvantGate({
budget: {
maxCostPerMinute: 0.50, // Circuit breaker: halts calls if threshold is reached
maxTokensPerTurn: 4000
},
pii: {
redact: ['email', 'credit-card'],
maskChar: '*'
},
fallback: [
'anthropic/claude-3-5-sonnet',
'openai/gpt-4o-mini'
]
});
Step 2: Safe Tool Execution & Workflow Interception
In an agentic workflow, you must never let the model invoke tools with unchecked parameters. You can intercept and validate both the step input and the agent's intent:
import { z } from 'zod';
// Define strict contract for the tool
const SearchDatabaseSchema = z.object({
query: z.string().max(200),
limit: z.number().int().min(1).max(20),
});
async function runSecuredAgentStep(userPrompt: string) {
// 1. AvantGate sanitizes input (PII redaction) & checks budget in-process
const safeExecution = await gate.run(async (context) => {
const response = await context.completion({
model: 'anthropic/claude-3-5-sonnet',
messages: [{ role: 'user', content: userPrompt }],
tools: [
{
name: 'search_database',
description: 'Search internal records',
parameters: SearchDatabaseSchema,
},
],
});
// 2. Validate agent tool calls before execution
if (response.toolCalls) {
for (const call of response.toolCalls) {
if (call.name === 'search_database') {
const validatedArgs = SearchDatabaseSchema.parse(call.args);
return await executeInternalSearch(validatedArgs);
}
}
}
return response.content;
});
return safeExecution;
}
If an injection attempts to inject DROP TABLE or trigger a recursive chain that blows past your token limits, avantGate halts the execution context before it wrecks your infrastructure.
4. Why In-Process Beats External LLMOps for 90% of Use Cases
| Concern | External LLMOps Stack | In-Process Control Plane (avantGate) |
|---|---|---|
| Infra overhead | Docker, Postgres, ClickHouse, Redis | Zero (Pure TypeScript module) |
| Network Latency | Additional network hops per call | Sub-millisecond (runs in memory) |
| Data Privacy | Prompts/logs stored in external databases | Zero egress (data never leaves your runtime) |
| Maintenance | Migrations, backups, version upgrades | Update avantGate |
Conclusion & Next Steps
Agents are the future of software engineering, but building agents without deterministic boundaries is an invitation for disaster. You don't need a 5-container infrastructure stack to build secure, cost-contained AI tools.
Check out the project and docs on GitHub:
👉 Avant Gate
- How are you currently handling runaway agent loops and tool validation in your stack? Let's discuss in the comments below! 🚀


Top comments (10)
The in-process approach is appealing, especially for teams that don’t need a separate observability stack just to enforce basic limits. But I’d be careful not to let “the guard runs locally” blur into “the data stays local”: if the model call still goes to an external provider, the protection depends on exactly what the sanitizer removes—and whether enough context remains for a useful answer.
I’d also separate valid input from authorized action. A Zod schema can rule out malformed arguments, but a perfectly valid query or write can still be outside the user’s permissions or the agent’s intended scope. That’s where least-privilege tools and checks against the actual caller’s authorization still matter.
For runaway loops, per-turn token and per-minute cost limits are a good start, but I’d want to know whether the budget is enforced across concurrent runs and fallback calls too. Otherwise the individual guardrails can all look sensible while the total spend still escapes the limit. How does avantGate account for those cases—and what does it do when PII redaction removes context the model needs to answer correctly?
Thanks for the sharp critique—you’ve hit on three of the most common failure modes in production agent architectures: the illusion of data locality, the gap between schema validation and authorization, and distributed cost leaks.
Here is a quick summary of how avantGate approaches each of these challenges today:
1. "Local Guard" vs. "Local Data" & Context Starvation
Running an in-process guardrail doesn't mean data stays local if prompts are dispatched to OpenAI or Anthropic. When PII redaction strips essential fields, the model can easily suffer from context starvation (inability to distinguish entities or relationships).
toClientData): In client-facing apps, the LLM rarely needs raw PII. Our tools project a lean structural summary to the model ({ status: "active", recordsFound: 2 }), while the raw PII payload bypasses the LLM entirely and streams directly to the authenticated client UI over secure HTTP/SSE.[IBAN_REF_#1],[PERSON_#2]). The model reasons accurately about equivalence without ever seeing raw data, and values can be re-hydrated downstream only when invoking authorized transactional tools.2. Valid Input vs. Authorized Action (BOLA / IDOR Defense)
Zod enforces syntactic correctness, not caller authority. An agent can emit a perfectly valid
{ invoiceId: "inv_9988" }that belongs to another tenant or exceeds the user's role.assertTenant,assertOwnership) before any business logic or DTO projection executes. Ifcaller.tenantId !== record.tenantId, aDataAccessDeniedErroris thrown immediately.AccessControlToolStrategydynamically filters the tool catalog against the authenticated caller's roles and domain. A non-admin user's LLM isn't just blocked from executingrefund_payment—the tool definition is completely omitted from the model's tool dictionary.requireApproval: true) checkpoints.3. Distributed Spends, Concurrency & Fallback Storms
Per-turn and per-minute rate limits protect individual loops, but fall short when 20 concurrent tasks spawn or when fallback models cascade.
PreflightBudgetGuardestimates the maximum token cost before the network call is dispatched and claims it against a shared tenant quota (SQLite with WAL locks). Concurrent runs trip the circuit breaker at the gate before burning spend.MultiModelFailover(e.g., GPT-4o ➔ Claude 3.5 Sonnet ➔ Mistral), subsequent retry attempts inherit and increment the parent session’stotalCostUsdandtotalTokens. If a session cap is $0.50 and the primary burns $0.40 before erroring, the fallback won't launch if it risks breaching the threshold.We'd love your thoughts:
If you have specific patterns or edge cases you've encountered around distributed budget locking or fine-grained caller authorization, we’d be eager to hear your suggestions for improving this further!
The strongest part of this design, to me, is that the controls sit at the point of execution: schemas shape inputs, caller-bound checks authorize actions, and the budget guard decides whether work can start.
One edge case I’d pressure-test is an ambiguous timeout: a provider may have accepted a request even though your app saw a failure, and then a retry or fallback begins. The reservation needs a stable identity across those attempts and a clear settle, release, or reconciliation path—otherwise you risk either overspending or stranding budget. Same principle for authorization: filtering tools from the model’s catalog is useful least privilege, but the check at the actual read or write remains the authority, especially for delayed retries where access may have changed. How do you handle that reservation lifecycle today?
Great pressure-test!
Rather than importing a heavyweight banking settlement engine (Hold ➔ Settle ➔ Clearing ➔ 2PC) designed for multi-day credit card holds, AvantGate solves the Overspending vs. Stranded Budget tradeoff using a pragmatic 3-pillar lifecycle:
Here is how we handle both edge cases without over-engineering a full banking settlement engine:
1. Authorization: Execution-Time Authority
Tool catalog filtering is just prompt shaping, not a security boundary.
When a delayed retry happens (e.g. via
useTask.retry()hours later or after human approval), authorization is re-evaluated at the actual point of execution inside the tool handler with the caller’s current session. If their role or tenant access was revoked in the meantime, it fails closed with an immediate error before any read or write.2. Budget Lifecycle on Ambiguous Timeouts
To avoid the Overspending vs. Stranded Budget trap, we use a 3-step pragmatic flow:
parentRunId ?? runId): The client and gateway preserve the original run ID across retries so all layers track the same operation.usage.total_tokens).Idempotency-Keysupport unlike OpenAI, our gateway caches completed runs by their causal ID. A client retry after a socket drop is served directly from the gateway cache without re-billing the model, with a strict retry ceiling (maxManualRetries: 3) capping worst-case exposure.The “preflight solvency, settle on actual usage” distinction is useful—it avoids turning a temporary network failure into stranded budget. The hardest seam seems to be when the provider completes the run but the gateway times out before it records the result: a retry could miss the cache and trigger a second execution. How do you handle that window, especially for providers without native idempotency keys?
Spot on—that’s the exact distributed systems seam. Unlike Stripe, most LLM endpoints (Anthropic, Gemini, Bedrock, Groq) don't have native HTTP idempotency caching for synchronous inference.
We tackle that phantom window across three layers:
Decouple Blast Radius (Inference vs. Tools)
A duplicate text completion is just a minor FinOps loss ($0.02). The real danger is duplicating mutative side-effects (
charge_card,db_write). AvantGate places an atomic claim lock at the tool execution boundary (withServerIdempotency+ storage adapter). Even if the model reruns and outputs the exact same tool call, the tool itself short-circuits.Stream-First Socket Timeouts
Most ambiguous timeouts happen when a monolithic 30s HTTP timeout drops mid-generation. We stream by default and switch to a Time-To-First-Token (TTFT) and socket idle read timeout. As long as tokens keep flowing, the connection stays alive.
Pessimistic FinOps Accounting (No Silent Retries)
On an unconfirmed timeout, the preflight reservation is held in
UNCONFIRMED_TIMEOUT—never blindly refunded. A manual retry is only allowed if the tenant can absorb the worst case. No automatic retry loops.(For long-running tasks >30s, we route to asynchronous batch endpoints with deterministic
custom_id: runId, making retries pure, non-mutative status polling.)Really appreciate you laying out all three layers. The distinction between a duplicate completion and a duplicated tool side effect is the key one: the former is a bounded cost; the latter can change customer or system state. Putting idempotency at the execution boundary, rather than trusting the model or retry loop, feels like the right place to contain that risk.
The claim-lock path raises the hardest edge case for me: suppose the tool succeeds, then the worker dies before the result is written back. On retry, how does the adapter distinguish “side effect completed, response lost” from “claim abandoned and safe to run”? Do you persist and replay the tool result against the same key, or does recovery depend on the storage adapter?
I also like the UNCONFIRMED_TIMEOUT treatment—holding the reservation is less friendly in the moment, but safer than refunding or retrying when the provider outcome is unknown. And using deterministic custom_id for long jobs makes retries status checks, not second executions. Where has the most operational complexity landed so far: lock recovery, or keeping tool adapters consistent?
Hello!
No, to be completely candid, our open source library avantGate doesn't currently handle distributed database transaction locking, worker crash recovery, or idempotency keys for side effects. Those are heavy infrastructure concerns typically handled by orchestration engines like Temporal or durable queue layers rather than an LLM security and PII control plane.
Saga Replay & Storage: However, avantGate can optionally integrate with a database backend to support saga-style tool execution replays, state tracking, and error handling for critical agent actions.
The three failure modes are a useful framing. I especially like making budget and tool contracts first class runtime controls instead of bolting observability on after the fact.
Thanks! That's exactly why I built it this way.
Observability only tells you the server was breached or your budget was drained after the fact. By enforcing deterministic tool contracts (Zod) and budget circuit-breakers directly in-process, we stop the damage before the side effect ever touches downstream systems or external APIs.
How are you currently managing boundaries around your tool calls?