Blog

  • Why AI Agents Are Just Fancy While Loops (And Why That’s Dangerous)

    Strip away the marketing, and most "autonomous AI agents" are just while (true) loops with an LLM call inside. That's not autonomy—that's an infinite retry with extra steps.

    TL;DR.

    • Most AI agent frameworks are unbounded loops with no formal state constraints.
    • Probabilistic outputs + infinite retries = guaranteed failures at scale.
    • State machines enforce deterministic transitions, making "impossible states impossible."

    The Dirty Secret of "Autonomous" Agents

    Most agent frameworks ship retry loops disguised as intelligence. When the LLM fails, they retry. When the tool call errors, they retry. There's no concept of "this state is invalid" because there's no concept of state at all.

    Here's what 90% of agent code looks like under the hood:

    async function runAgent(task: string) {
      let attempts = 0
      while (attempts < MAX_RETRIES) {
        try {
          const response = await llm.chat(task)
          const toolCall = parseToolCall(response)
          if (toolCall) {
            const result = await executeTool(toolCall)
            task = `Previous result: ${result}. Continue.`
          } else {
            return response
          }
        } catch (e) {
          attempts++
          // Hope it works next time
        }
      }
      throw new Error('Agent failed')
    }

    This is a while loop with hope as an error handling strategy.

    The Production Failure

    I watched a "production-ready" agent framework rack up $400 in API costs in 12 minutes because it
    kept retrying a malformed tool call. The LLM was confidently generating the same invalid JSON on
    every attempt. No circuit breaker. No state validation. Just vibes.

    Why Probabilistic Logic Needs Deterministic Guardrails

    LLMs are probabilistic—they can output anything. If you don't constrain what transitions are legal, you're betting your system's reliability on luck.

    The fundamental problem:

    Probabilistic (LLM) Deterministic (State Machine)
    "Might" produce valid output Must be in a valid state
    Retries until success or timeout Transitions only if guard passes
    Invalid states are "rare" Invalid states are impossible

    When you combine a probabilistic system (LLM) with an unbounded loop, you get chaos that looks like it's working until it catastrophically fails.

    The Fix: State Machines as Agent Guardrails

    State machines don't replace your agent logic—they constrain it. Every action the agent takes must correspond to a legal transition.

    Here's the same agent with state constraints:

    const agentMachine = createMachine({
      id: 'agent',
      initial: 'idle',
      context: { task: null, attempts: 0, result: null },
      states: {
        idle: {
          on: { START: { target: 'thinking', actions: 'assignTask' } },
        },
        thinking: {
          invoke: {
            src: 'callLLM',
            onDone: [
              { target: 'executing', cond: 'hasToolCall' },
              { target: 'completed', cond: 'hasAnswer' },
              { target: 'failed' }, // No infinite loop
            ],
            onError: [
              { target: 'thinking', cond: 'canRetry', actions: 'incrementAttempts' },
              { target: 'failed' },
            ],
          },
        },
        executing: {
          invoke: {
            src: 'executeTool',
            onDone: { target: 'thinking' },
            onError: { target: 'failed' }, // Tool errors are terminal
          },
        },
        completed: { type: 'final' },
        failed: { type: 'final' },
      },
    })

    Key differences:

    1. Explicit failure states – No more "retry forever"
    2. Guard conditionscanRetry checks attempt count before allowing retry
    3. Terminal states – The machine must end in completed or failed
    4. No implicit transitions – Every path is visible and testable
    Production Win

    After switching to state machines, our agent error rate dropped from ~8% to under 0.5%. Not
    because the LLM got smarter—because we stopped allowing invalid state transitions. The bugs were
    in our loop logic, not the model.

    The "Impossible States" Guarantee

    With a state machine, you can prove that certain states are unreachable. That's not a nice-to-have—it's a requirement for production systems.

    Consider this invalid scenario:

    • Agent is "executing" a tool
    • But context.task is null
    • And context.attempts is negative

    In a while-loop agent, this state is technically possible (bugs happen). In a state machine, it's mathematically impossible because:

    1. You can only reach executing from thinking
    2. thinking can only be reached from idle with a valid task
    3. attempts can only be modified by the incrementAttempts action

    This is called state space reduction—and it's why avionics software and financial systems use state machines.

    When to Use This Pattern

    Use state machines for any agent that runs in production, handles money, or operates without human supervision.

    Use State Machines Don't Bother
    Production agents One-off scripts
    Multi-step workflows Simple Q&A chatbots
    Anything with retry logic Stateless API wrappers
    User-facing automation Internal dev tools

    The Bottom Line

    "Autonomous AI" is marketing. Every production system needs constraints. State machines provide those constraints without sacrificing flexibility.

    The next time someone pitches you an "autonomous agent framework," ask one question: "What prevents it from looping forever?"

    If the answer involves the word "usually" or "timeout," you're looking at a while loop with marketing.


    Start with enforcement primitives: rules + configs you can paste today: /resources.

    Building deterministic AI systems? See how Ranex enforces code policies without the guesswork.

  • The Architect’s Guide to Privacy-First AI Coding (2025)

    If you're trying to use AI without shipping your repo to someone else's computer, you're not "paranoid" — you're doing your job.

    Video coming soon

    TL;DR.

    • Default to local-first tools for code + secrets.
    • Add redaction + denylist for anything that could leak (.env*, keys, stack traces).
    • Use policy + logging so exceptions are visible and reversible.

    Table of Contents

    Use this table of contents to jump straight to the control you need.

    What does “privacy-first AI coding” actually mean?

    Privacy-first AI coding means designing workflows where source code and secrets stay in your controlled environment by default, and any exceptions are explicit, minimal, and auditable.

    In plain English: you can use AI, but the default posture is “assume everything you paste could leak.”

    What are the biggest ways developers leak code to AI tools?

    Common leaks happen through copy/paste (snippets, stack traces, configs), IDE plugins with unclear data handling, and logs that capture prompts or responses.

    In my experience, the leak rarely looks like “someone exfiltrated our repo.” It looks like:

    • a helper script prints env vars into a terminal recording
    • a CI log contains a token
    • a support paste includes a proprietary stack trace

    How do you build a privacy-first AI stack without killing developer velocity?

    Use a tiered setup: local-first by default, redaction for unavoidable sharing, and explicit approval gates for anything that leaves the machine.

    A workable pattern:

    • Tier 0 (default): local model + local index
    • Tier 1: redacted snippets for external LLMs (only when needed)
    • Tier 2: strict, audited exceptions (e.g., vendor security review + enterprise contract)

    What should run locally vs what can be cloud?

    Keep code, secrets, and dependency graphs local; only send minimal redacted context to the cloud when you can prove it contains no sensitive data.

    Privacy-First Air-Gap Workflow

    A simple rule I use:

    • if it can identify your product, your customers, or your infrastructure, it’s sensitive
    • if it includes auth, tokens, internal URLs, or error traces, it’s sensitive

    How do you prevent .env and secrets from ever touching prompts?

    Treat secrets as a separate classification: block them at the editor, block them at the CLI, and block them at the policy layer before prompts are sent.

    Practical controls:

    • pre-commit scanning (secrets + high-risk patterns)
    • redaction middleware for prompts and logs
    • denylist file globs: .env*, *.pem, id_rsa, *.key

    Download the guardrail configs (Cursor rules + MCP) in /resources and stop
    relying on discipline.

    How to Enforce This Without Micro-Managing

    Privacy-first AI coding needs an index and policy enforcement, otherwise "local-first" becomes "local-ish" the moment someone copies the wrong file into a prompt.

    Ranex automates this governance so you don't have to play bad cop. It acts as a guardrail that makes your privacy policy enforceable in code, not just in docs.

    What to do next

    Start with one rule you can enforce today: never paste .env or stack traces into external tools, and build the workflow to make that rule effortless.

    The best privacy posture is the one your team actually follows. Make the safe path the easy path, and you won't have to rely on vigilance alone.

  • The Ultimate Enterprise FastAPI Project Structure (2025)

    If you want FastAPI to scale, the folder structure isn’t bikeshedding — it’s your future incident report in slow motion.

    If you want the "why" behind every folder, copy‑paste templates, and the rules that keep AI-generated code from freelancing inside your routers, you're in the right place.

    Video coming soon

    TL;DR.

    • Use a feature-first layout under domain/.
    • Keep routers thin: validate input, call a service, return a response.
    • Enforce import boundaries in CI so the structure stays intact over time.

    Want the ready-to-copy rules/configs? Grab the Cursor/Windsurf rules and templates here:
    /resources.

    Table of Contents

    Use this table of contents to jump directly to the section you need without reading the whole article.

    What is the best folder structure for FastAPI?

    The best FastAPI structure separates domain, routers, and dependencies, keeps business logic out of route handlers, and uses dependency injection to avoid circular imports.

    Here’s a pragmatic enterprise layout that works for monorepos, services, and long-lived products:

    FastAPI Enterprise Layered Architecture

    If your first reaction is “where did crud.py go?”, that’s the point: you’re forcing clear boundaries instead of letting CRUD helpers become a dumping ground.

    The anti-patterns that quietly kill FastAPI projects

    A common FastAPI failure mode is routers accumulating business logic and data access, which makes code harder to test, increases coupling, and encourages “just this one exception” drift.

    Most FastAPI projects don’t fail because the code is “bad.” They fail because the architecture makes it impossible to stay good under pressure.

    Why your models.py file is a ticking time bomb

    If models.py becomes “the place where everything lives,” it turns into:

    • A dependency magnet (everything imports it)
    • A circular import factory (because it imports everything back)
    • A schema drift generator (because Pydantic models start representing both transport and domain)

    Fix: split by feature under domain/ and keep Pydantic schemas in schemas.py per feature.

    Stop putting DB calls in your routers

    Router handlers should coordinate:

    • validate input
    • call a service
    • return a response

    When routers do DB work, you get:

    • untestable logic (you can’t unit test without a DB)
    • inconsistent security checks
    • “just this one exception” copy-paste everywhere

    Fix: put business logic in domain/<feature>/service.py and data access in domain/<feature>/repo.py.

    The architecture: a mental model you can enforce

    A good FastAPI structure forces good decisions by making the “right” dependency direction the easiest path, so features scale without turning routers into a dumping ground.

    The point of structure isn’t aesthetics — it’s forcing good decisions.

    Domain-Driven Design (Lite): group by feature, not file type

    Feature grouping is the only pattern that survives:

    • multiple teams
    • multiple databases
    • “we added billing”
    • “we renamed organizations to workspaces”

    A folder like domain/users/ is a boundary. A folder like services/ is a confession.

    The services layer: where the actual logic lives

    The service layer is the only place where:

    • invariants belong (e.g., “cannot delete the last admin”)
    • workflows belong (e.g., “create user → send email → write audit log”)
    • integration is coordinated

    Your routers should read like orchestration, not like a novella.

    The contracts layer: where you stop lying to yourself

    “Contracts” is where you define stable outputs:

    • response shapes
    • error codes
    • public schemas used across routers

    This is also where AI-written code tends to drift first — it’ll happily introduce a new response shape on Tuesday because it “felt right.”

    If you want deterministic systems, contracts must be boring.

    The tooling: how to enforce the structure automatically

    Enforce FastAPI architecture with import boundaries: routers can’t touch DB/session, services can’t import FastAPI, and violations should fail CI automatically.

    You can enforce architecture manually in PR review… until you can’t.

    Here’s the enforcement model:

    • routers must not import DB/session
    • services must not import FastAPI
    • domain must not import routers

    The pitch

    You can review PRs manually, or you can use Ranex to reject any commit that imports db into routers.

    That’s the Trojan Horse: you came here for a folder tree. You leave with a workflow that prevents the tree from turning into a jungle.

    Quick start: how to apply this structure to an existing messy repo

    Refactor by moving logic out of routers first, then isolating data access, then locking the rules with tooling so the structure stays fixed.

    1. Create domain/<feature>/service.py and move logic out of routers.
    2. Create domain/<feature>/repo.py and move DB access behind an interface.
    3. Create dependencies/ and put FastAPI deps there (session, auth, current_user).
    4. Add “contract” response types and stop returning raw ORM objects.
    5. Enforce imports with tooling so it stays fixed.
  • Deterministic Code Analysis: How to Stop AI Hallucinations in Real Codebases

    If you've ever watched an assistant confidently recommend an import path that doesn't exist, you already know the problem: probabilistic retrieval doesn't respect your codebase's ground truth.

    Video coming soon

    TL;DR.

    • Deterministic code intelligence = reproducible answers tied to real symbols.
    • Similarity search can return “plausible” context; code needs graph truth (defs/refs/calls).
    • A practical approach is hybrid: deterministic retrieval + LLM synthesis + validation.

    Table of Contents

    Use this table of contents to jump to the exact claim you’re trying to validate.

    What is deterministic code intelligence?

    Deterministic code intelligence means the same query against the same commit produces the same evidence-backed answer, derived from parsing and indexing the code — not guessing from similarity.

    That usually implies:

    • parsing the code (AST, symbols)
    • building an index (definitions, references)
    • answering with citations (file + symbol locations)

    Why does "RAG for code" drift in practice?

    Vector search is similarity-based, not structure-based. It returns "vibes," not facts. When function names repeat or patterns look alike, RAG retrieves the plausible context, not the correct context.

    Deterministic Graph vs Probabilistic Vector

    This isn't a bug; it's the math of cosine similarity. But for an AI Agent, "close enough" is dangerous.

    The "Ambiguity Trap" (Why Agents Crash)

    A common failure mode is Shadow Functions: two functions with similar docstrings where only one is valid in the current scope. Static analysis solves this instantly. Vector search flips a coin.

    Here's a classic Namespace Collision that breaks Vector Search in production:

    # payments/service.py
    
    def charge_card(user_id: str) -> None:
        """Charge a card for a subscription."""
        ...
    
    
    def charge_card_test_mode(user_id: str) -> None:
        """Charge a card for a subscription (test mode)."""
        ...

    If an Agent asks "Where do we charge users?", a Vector Database sees two nearly identical semantic embeddings. It flips a coin.

    If it retrieves charge_card_test_mode, your Agent might write code that mocks payments in production. RAG drift becomes data corruption.

    A deterministic index asks a different question: "Who calls charge_card in the production environment?" That's not a probability. That's a graph query.

    What should you use instead of pure RAG?

    Use a hybrid: deterministic indexing for code structure (symbols, imports, calls), plus language models for synthesis and explanation on top of verified evidence.

    The winning pattern:

    • Deterministic Retrieval: Locate symbols via AST (Abstract Syntax Tree).
    • Scoped Context: Feed the LLM only the relevant slice.
    • LLM Synthesis: Let the Agent explain the code, not find it.
    • Validation: Tests, linters, policy gates before shipping.

    How does this connect to Ranex Atlas?

    Ranex Atlas is useful when you want code answers tied to real symbols and dependency edges, so the assistant can’t “invent” a module that isn’t in your repo.

    That’s the entire point of deterministic intelligence: less roulette, more reproducibility.

    What to do next

    Start by choosing one deterministic question you want answered reliably — "where is this symbol defined?" or "what calls this?" — and build the index around that.

    Once you have that working, expand from there. The goal isn't to replace your entire search stack overnight; it's to prove that deterministic answers are possible for the questions that matter most to your team.

    If you're setting this up now, start with the ready-made IDE rules + configs in
    /resources.

  • AI Governance Is Just “Don’t Get Sued by OpenAI” (Here’s the Checklist)

    "Governance" sounds like a compliance checkbox. It's not. It's the thing that saves you when your AI assistant accidentally commits your .env file to a training dataset, or when a prompt injection exfiltrates your customer list.

    TL;DR.

    • Governance = liability shield. If you can't prove what your AI touched, you can't defend yourself.
    • OpenAI, Anthropic, and Google all have ToS clauses about what you can send them. Violate them, lose your API access.
    • The EU AI Act is coming. If you're not logging AI decisions now, you're building legal debt.

    The Three Ways AI Will Get You Sued

    AI governance exists because AI creates new liability vectors that traditional security doesn't cover: data leakage to model providers, hallucinated compliance violations, and unauditable decision-making.

    1. Data Leakage to Model Providers

    Every prompt you send to OpenAI, Anthropic, or Google is data you're transmitting to a third party. If that prompt contains:

    • Customer PII
    • Source code under NDA
    • API keys or secrets
    • Internal financial data

    …you've potentially violated your own privacy policy, your customer contracts, and possibly GDPR.

    Real Incident

    A Fortune 500 company's legal team discovered engineers had been pasting customer support
    tickets—including full names, emails, and account numbers—into ChatGPT to "summarize" them. No
    malice. Just convenience. The remediation cost: $2.3M in notifications and credit monitoring.

    2. ToS Violations That Kill Your API Access

    OpenAI's Terms of Service explicitly prohibit:

    • Using outputs to train competing models
    • Generating content that violates laws in your jurisdiction
    • Submitting data you don't have rights to process

    If you violate these, you don't get a warning. You get a terminated API key and a legal letter. Your "AI-powered product" is now a landing page with a 500 error.

    3. The EU AI Act Audit Trail Requirement

    Starting in 2025, the EU AI Act requires documentation of AI system behavior for high-risk applications. If your AI makes decisions about:

    • Employment
    • Credit/lending
    • Access to services

    You need logs. Not "we probably have logs somewhere." Actual, auditable, timestamped evidence of what the AI saw and what it decided.

    The Minimum Viable Governance Checklist

    The minimum controls are: data classification, secret blocking, prompt boundaries, output validation, and audit logging. Miss any of these and you're flying blind.

    Data Classification (What's Sensitive?)

    Before you can protect data, you need to define it:

    Classification Examples AI Policy
    Critical API keys, credentials, PII Never send to external LLMs
    Confidential Source code, internal docs Redact before sending
    Internal Meeting notes, drafts Log but allow
    Public Marketing copy, docs No restrictions

    Secret Blocking (The First Line of Defense)

    Block sensitive files at the source. Don't rely on humans to remember.

    const BLOCKED_PATTERNS = ['.env*', '*.pem', '*.key', 'id_rsa*', '*credentials*', '*secret*']
    
    function canSendToLLM(filepath: string): boolean {
      return !BLOCKED_PATTERNS.some(pattern => minimatch(filepath, pattern))
    }

    Prompt Boundaries (What Can Talk to the Cloud?)

    Define which tools can send prompts externally:

    • ✅ Approved IDE extensions (with logging)
    • ✅ Internal chatbot (with redaction middleware)
    • ❌ Random npm packages with LLM calls
    • ❌ Browser extensions that "summarize" your screen

    Output Validation (Don't Ship Hallucinations)

    AI-written code must pass the same gates as human code:

    • Linting
    • Type checking
    • Security scanning
    • Code review

    If it didn't pass CI, it doesn't ship. Period.

    Audit Logging (Prove What Happened)

    Log enough to reconstruct incidents, but not enough to create a new data leak:

    interface AIAuditLog {
      timestamp: string
      userId: string
      toolName: string
      action: 'prompt_sent' | 'response_received' | 'blocked'
      promptHash: string // Hash, not content
      policyDecision: 'allowed' | 'redacted' | 'blocked'
      redactedFields?: string[] // What was removed
    }

    The "We Got Hacked" Scenario

    When (not if) something goes wrong, governance is the difference between "we have logs" and "we have no idea what happened."

    Imagine this call:

    "Hey, we found our internal API in a public GitHub repo. We think it came from an AI tool. Can you tell us what happened?"

    Without governance: "Uh… we don't really track that. Let me ask around?"

    With governance: "Give me 10 minutes. I'll pull the audit logs and tell you exactly which tool, which user, and which prompt sent that data."

    The second response is the one that doesn't end in a lawsuit.

    References

    These frameworks aren't optional reading—they're the basis for compliance audits.


    Free setup kit: grab the Cursor/Windsurf rules + CI templates in /resources.

    Need to enforce these policies automatically? Ranex turns governance checklists into code-level guardrails.