Agent Series (2): ReAct — The Most Important Agent Reasoning Paradigm

You Think Your Agent Is “Thinking.” It’s Actually Just Predicting Tokens.

Here’s a scenario that happens more often than you’d think.

You ask an Agent to write a competitive analysis report. It confidently outputs three professional-looking pages — complete with data, conclusions, and strategic recommendations.

There’s just one problem: every number comes from its training data, which may be a year old. It didn’t search. It didn’t verify. It just generated text that sounds authoritative.

That’s not thinking. That’s fluent hallucination.

Chain-of-Thought (CoT) has the same fundamental problem. CoT prompting tells the model to “reason step by step” before answering, and it genuinely does improve accuracy on many tasks. But the model is still reasoning entirely within language space. It can generate a very coherent chain of thought that leads to a completely wrong answer — because its only information source is training data.

ReAct was built to solve this.

ReAct: Reasoning + Acting, Interleaved

In 2022, researchers from Princeton and Google published ReAct: Synergizing Reasoning and Acting in Language Models.

The core idea is elegantly simple: let the model alternate between reasoning and acting, rather than reasoning first then acting, or acting without reasoning.

The concrete form is a three-part loop:

Thought  →  Action  →  Observation
   ↑                         │
   └─────────────────────────┘
  • Thought: What the model is “thinking” — current analysis, what to do next, why
  • Action: The actual tool call and parameters
  • Observation: The real result returned by the tool

The critical mechanism: Observation is fed back into the model as new context, allowing it to reason based on actual results. This creates the “think → act → observe → think again” loop.

This one loop fixes CoT’s core flaw: the model is no longer reasoning in isolation. It can interact with the real world and update its reasoning based on real feedback.

A Concrete Example: Watching an Agent “Think”

I built a complete ReAct Agent demo using LangGraph + GLM-4-Flash with two tools: calculator (safe math evaluator) and web_search (Bing search).

Code: agent-01-react-agent/react_agent.py

Here’s an actual execution trace — Demo 3: search for the areas of Beijing and Shanghai, then calculate the difference.

════════════════════════════════════════════════════════════
  Demo 3 ▸ Multi-Round Search (Same Tool, Multiple Calls)
════════════════════════════════════════════════════════════

[User Question]
  First search for Beijing's area, then Shanghai's area,
  then calculate how much larger Beijing is in km².
────────────────────────────────────────────────────────────

[Step 1] THOUGHT → ACTION
  Action  : web_search(query='北京面积 平方公里')

  Observation : • Beijing area: Total area 16,410.54 km²...
────────────────────────────────────────────────────────────

[Step 2] THOUGHT → ACTION
  Action  : web_search(query='上海面积 平方公里')

  Observation : • Shanghai area: Land area approximately 6,340.5 km²...
────────────────────────────────────────────────────────────

[Step 3] THOUGHT → ACTION
  Action  : calculator(expression='16410.54 - 6340.5')

  Observation : 10070.04
────────────────────────────────────────────────────────────

[Final Answer]
  Beijing's area is approximately 16,410.54 km², Shanghai's is
  approximately 6,340.5 km². Beijing is about 10,070.04 km² larger.
════════════════════════════════════════════════════════════

Notice what happened here:

  1. The Agent decided on its own to search Beijing first, then Shanghai, then calculate — no hardcoded execution order
  2. Each search result (Observation) was read by the model and used to determine the next step
  3. The final calculation used real numbers extracted from real searches

This is ReAct’s value: the execution path is planned dynamically at runtime, not hardcoded by the developer in advance.

ReAct vs. Chain-of-Thought: A Direct Comparison

Aspect Chain-of-Thought ReAct
Information source Training data only Training data + tool results
Execution path Reasoning in language space Think → real action → observe results
Can access real-time data ✗ ✓ (via tools)
Can execute computation/code ✗ ✓ (via tools)
Reasoning verifiable Hard to verify Each Observation is a real result
Risk of side effects Low (no actions) High (requires safety boundaries)

One sentence summary: CoT makes the model think clearly. ReAct makes it think while doing.

Building a ReAct Agent with LangGraph

Here’s the core implementation. The code uses LangGraph’s create_react_agent — one of the cleanest ReAct implementations available.

1. Safe Calculator Tool

import ast
import operator
from typing import Any
from langchain_core.tools import tool

_SAFE_OPS: dict[type, Any] = {
    ast.Add:  operator.add,
    ast.Sub:  operator.sub,
    ast.Mult: operator.mul,
    ast.Div:  operator.truediv,
    ast.Pow:  operator.pow,
    ast.Mod:  operator.mod,
    ast.USub: operator.neg,
}

def _eval_ast(node: ast.AST) -> float:
    if isinstance(node, ast.Constant):
        return float(node.value)
    if isinstance(node, ast.BinOp):
        op_fn = _SAFE_OPS.get(type(node.op))
        if op_fn is None:
            raise ValueError(f"Unsupported operator: {type(node.op).__name__}")
        return op_fn(_eval_ast(node.left), _eval_ast(node.right))
    if isinstance(node, ast.UnaryOp):
        op_fn = _SAFE_OPS.get(type(node.op))
        return op_fn(_eval_ast(node.operand))
    raise ValueError(f"Unsupported AST node: {type(node).__name__}")

@tool
def calculator(expression: str) -> str:
    """Evaluate a math expression. Supports + - * / ** % and parentheses."""
    try:
        tree = ast.parse(expression.strip(), mode="eval")
        result = _eval_ast(tree.body)
        if result == int(result):
            return str(int(result))
        return f"{result:.6g}"
    except (ValueError, SyntaxError, ZeroDivisionError) as e:
        return f"Calculation error: {e}"

Why not just use eval()?

eval("__import__('os').system('rm -rf /')") — that line will execute a deletion on your machine. Tools are the Agent’s “hands.” Once an attacker manipulates the LLM through prompt injection, eval() becomes a direct path to your system.

AST parsing only allows math operation nodes — everything else is rejected. This is the foundational principle of safe tool design.

2. Web Search Tool

import requests
from bs4 import BeautifulSoup
from urllib.parse import quote

_BING_HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
        "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
    ),
    "Accept-Language": "en-US,en;q=0.9",
}

@tool
def web_search(query: str) -> str:
    """Search the web and return the 3 most relevant snippets."""
    try:
        url = f"https://www.bing.com/search?q={quote(query)}&setlang=zh-CN"
        resp = requests.get(url, headers=_BING_HEADERS, timeout=10)
        resp.raise_for_status()

        soup = BeautifulSoup(resp.text, "html.parser")
        snippets = []
        for li in soup.find_all("li", class_="b_algo")[:4]:
            h2 = li.find("h2")
            title = h2.get_text(strip=True) if h2 else ""
            p = li.find("p")
            body = p.get_text(strip=True) if p else ""
            if title or body:
                snippets.append(f"• {title}: {body}"[:200])

        return "n".join(snippets[:3]) if snippets else "No results found."
    except requests.RequestException as e:
        return f"Search failed: {e}"

3. Building the Agent

import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
# LangGraph V1.0 moved create_react_agent to chat_agent_executor submodule
from langgraph.prebuilt.chat_agent_executor import create_react_agent

load_dotenv()

llm = ChatOpenAI(
    base_url="https://open.bigmodel.cn/api/paas/v4",
    api_key=os.getenv("LLM_API_KEY"),
    model="glm-4-flash",
    temperature=0,
)

agent = create_react_agent(
    model=llm,
    tools=[calculator, web_search],
)

result = agent.invoke(
    {"messages": [("user", "How much larger is Beijing than Shanghai in km²? Search and calculate.")]},
    config={"recursion_limit": 20},
)
print(result["messages"][-1].content)

Three core lines: define tools → bind LLM → run. LangGraph handles all the message routing, tool call dispatch, result injection, and loop control under the hood.

The correct import path for create_react_agent

LangGraph V1.0 moved this function to langgraph.prebuilt.chat_agent_executor. Importing from langgraph.prebuilt triggers a LangGraphDeprecatedSinceV10 warning. Use the new path:

# ✅ Recommended
from langgraph.prebuilt.chat_agent_executor import create_react_agent

# ⚠️ Triggers deprecation warning
from langgraph.prebuilt import create_react_agent

How the Message Flow Actually Works

To truly understand ReAct, you need to see the underlying message sequence. Here’s what the LLM receives at the start of each cycle:

Context passed to LLM at round N:
┌─────────────────────────────────────────────────────┐
│ [System]  You are an assistant with these tools:    │
│           calculator, web_search                    │
│                                                     │
│ [Human]   Question: How much larger is Beijing?     │
│                                                     │
│ [AI]      (tool call) web_search("Beijing area")   │  ← Round 1 Action
│ [Tool]    Beijing area: 16,410 km²                 │  ← Round 1 Observation
│                                                     │
│ [AI]      (tool call) web_search("Shanghai area")  │  ← Round 2 Action
│ [Tool]    Shanghai area: 6,340 km²                 │  ← Round 2 Observation
│                                                     │
│ ← LLM decides what to do next here →               │
└─────────────────────────────────────────────────────┘

Each cycle, the entire history is passed to the LLM. The model “sees” all previous thoughts and observations, then decides:

  • Continue calling tools (more information needed)
  • Stop and deliver a final answer (enough information gathered)

This is why it’s called a loop — the model itself is the loop’s termination condition. It decides when to stop.

When Things Go Wrong: Failure Modes and Guards

The same “decide when to stop” design that makes ReAct powerful also introduces a risk: if the model misjudges, the loop never terminates.

Common runaway scenarios:

Scenario 1: Tool keeps failing, model keeps retrying

Action: web_search("vague ambiguous query")
Observation: No results found
Thought: Let me try different keywords
Action: web_search("different keywords")
Observation: No results found
Thought: Maybe one more variation...
(infinite loop)

Scenario 2: Model misunderstands the task and pursues the wrong direction

Thought: I need the exact value of X
Action: calculator("...")
Observation: Approximate result
Thought: Not precise enough, I need more decimal places
Action: calculator("...")
(infinite pursuit of "precision")

Scenario 3: Tools form a circular dependency

Thought: I need to know A before I can look up B
Action: search(A)
Observation: Requires knowing B first
Thought: I need to know B before I can look up A
(circular dependency)

LangGraph’s recursion_limit parameter is the hard safety net:

result = agent.invoke(
    {"messages": [("user", question)]},
    config={"recursion_limit": 5},  # Force-stop after 5 steps
)

When the step count exceeds the limit, LangGraph raises GraphRecursionError:

[recursion_limit triggered]
  Exception type: GraphRecursionError
  Message: Recursion limit of 5 reached without hitting a stop condition...

→ Conclusion: Always set a reasonable recursion_limit in production (15~25 recommended)
→ Too low: legitimate tasks get cut off; Too high: runaway Agent burns massive tokens

How to set recursion_limit

  • Simple tasks (single tool call): 5–8 steps is enough
  • Medium tasks (multi-tool, multi-step): 10–15 steps
  • Complex research tasks: 20–25 steps
  • Tasks requiring 30+ steps should reconsider architecture — you may need multi-Agent collaboration (covered in a later article)

The rule of thumb: set it to roughly 2× the number of steps a successful execution needs. Room to breathe, but a real ceiling.

Five Demo Scenarios: From Simple to Complex

The complete code includes 5 progressive demos covering the main ReAct usage patterns:

Demo 1: Pure Calculation (single tool, single step)

Question: Calculate (1024 * 768) + (1920 * 1080)
Steps: calculator('(1024 * 768) + (1920 * 1080)') → 2860032

Validates the basic tool-calling pipeline.

Demo 2: Search + Calculate (multi-tool, multi-step)

Question: What year were Python and JavaScript first released? Calculate the difference.
Steps: web_search("Python release year") → web_search("JavaScript release year") → calculator

Shows the Agent autonomously orchestrating different tools in the right order.

Demo 3: Multi-round Search (same tool, multiple calls)

Question: How much larger is Beijing than Shanghai in km²?
Steps: web_search("Beijing area") → web_search("Shanghai area") → calculator → 10070.04

Shows the Agent deciding what to search second based on what it found first.

Demo 4: No Tools Needed (direct answer)

Question: Explain the ReAct paradigm in one sentence.
Steps: No tool calls — direct answer

Shows the Agent knowing when not to call tools. This matters as much as knowing when to call them.

Demo 5: Trigger recursion_limit (safety net demo)

Question: Search Python/Java/C release years, calculate the sum (~10 steps needed)
Limit: recursion_limit=5
Result: GraphRecursionError (correctly triggered)

Production safety mechanism verification.

An Interesting Observation: Agents Can “Luck Into” Correct Answers

Demo 2 produced a result worth documenting carefully.

The Agent searched for JavaScript’s release year. The Bing snippet it received came from an article published in 2023 that mentioned Python’s 1991 origin. The model appears to have confused “2023” (article publication date) with JavaScript’s release year. The calculation step ran 2023 - 1991 = 32, returning 32.

But the final answer was correct: “Python was released in 1991, JavaScript in 1995 — a 4-year difference.”

The model overrode its (incorrect) calculation result with its internal training knowledge and delivered the right answer.

This reveals a subtle property of ReAct: an Agent’s reasoning chain and its final answer can be decoupled. The model may make errors during tool calls, then “self-correct” in the final answer generation using built-in knowledge.

As an outcome, this is fine — you got the right answer. From an engineering perspective, it’s a problem. If you need traceable, verifiable conclusions, “it happened to be correct” isn’t sufficient. This is one of the challenges that Harness Engineering addresses (covered in a later article in this series).

Trace Visualization: Making Agent Reasoning Observable

A common production pain point: when something goes wrong, you don’t know which step failed, because only the final answer is visible by default.

Good practice: print the full Thought/Action/Observation sequence as a readable Trace:

from langchain_core.messages import AIMessage, HumanMessage, ToolMessage

def print_trace(result: dict) -> None:
    for msg in result["messages"]:
        if isinstance(msg, HumanMessage):
            print(f"[USER] {msg.content}")

        elif isinstance(msg, AIMessage):
            content = msg.content if isinstance(msg.content, str) else ""
            if msg.tool_calls:
                for tc in msg.tool_calls:
                    args = ", ".join(f"{k}={repr(v)}" for k, v in tc["args"].items())
                    print(f"[ACTION] {tc['name']}({args})")
            else:
                print(f"[FINAL ANSWER] {content.strip()}")

        elif isinstance(msg, ToolMessage):
            obs = msg.content if isinstance(msg.content, str) else str(msg.content)
            print(f"[OBSERVATION] {obs.strip()[:300]}")

GLM-4-Flash content field pollution

When using GLM-4-Flash, you may occasionally see raw JSON in AIMessage.content — something like {"index": 0, "delta": ...}. This is the model leaking internal streaming delta data into the content field.

Fix: detect when content starts with { or [ and can be parsed by json.loads(), then discard it.

def _clean_thought(text: str) -> str:
    stripped = text.strip()
    if stripped and stripped[0] in ("{", "["):
        try:
            json.loads(stripped)
            return ""  # leaked JSON, discard
        except json.JSONDecodeError:
            pass
    return text

The complete demo code already includes this handling.

The Limitations of ReAct

ReAct is powerful, but it’s not a silver bullet. Knowing its limits helps you use it correctly.

1. Context window fills up fast

Each cycle packs the entire history into context. Step count grows, token consumption spikes. Complex tasks (20+ steps) may fail on models with limited context windows.

2. Tool descriptions drive everything — write them well

ReAct relies entirely on the LLM understanding tool documentation to decide which tool to call and with what parameters. Vague docstrings lead to wrong tool selection. Tool descriptions are the invisible API of a ReAct system — treat them like API documentation.

3. No global planning capability

Standard ReAct is greedy: each step only looks at the current state to decide the next move, with no “plan the whole thing first, then execute” capability. For tasks requiring long-horizon planning (like writing an entire codebase), this can get stuck in local optima. This is what the Plan-and-Solve paradigm addresses (Article 3 in this series).

4. Poor fault tolerance for tool failures

When a tool returns an error, the model has to infer the next step from the error message alone. There’s no predefined retry strategy or fallback logic. This needs to be handled at the tool design level and the Harness layer.

Interview Prep: Articulate How Your Agent “Thinks”

Common question: How does your Agent decide its next action?

Many candidates answer “it calls tools.” But what the interviewer actually wants to hear is: who decides which tool to call, and when does it stop?

A clear answer framework:

“We use the ReAct paradigm. The core is a Thought → Action → Observation loop. At each step, the LLM looks at the full context — user question plus all previous Observations — and decides the next Action. The tool runs, its result is injected as a ToolMessage, and the model reasons again.

The loop terminates when the LLM judges it has enough information and stops calling tools, generating the final answer directly.

To prevent runaway loops, we set recursion_limit (typically 15–25). When it’s exceeded, we catch the exception and fall back to a degraded response. We also log the full Trace — every Action and Observation — so we can replay the entire reasoning chain when debugging.”

Key differentiators: mentioning Trace observability and recursion_limit shows you’ve thought beyond demos and considered production stability.

Summary

Three things from this article:

  1. ReAct = Reasoning + Acting, interleaved: The Thought → Action → Observation loop lets Agents update their reasoning based on real-world feedback. The fundamental difference from CoT: actions produce real results that feed back into the reasoning process.

  2. Tool design is ReAct’s invisible interface: Docstring quality directly determines how accurately the LLM selects tools. Safe implementation (AST instead of eval) determines whether the system boundary holds.

  3. recursion_limit is a required production setting: The model decides when to stop — that’s inherently risky. recursion_limit is the last line of defense. Recommended value: roughly 2× the steps needed for successful completion.

Next up: Agent Series Article 3 — Plan-and-Solve: When ReAct Isn’t Enough, How Agents Plan Before Acting. We’ll see where ReAct’s greedy strategy hits its ceiling on complex tasks, and how introducing an explicit planning layer breaks through it.

References

Welcome to visit my personal homepage for more useful knowledge and interesting products

Open Source Project (No.73): Sub2API – All-in-One Claude/OpenAI/Gemini Subscription-to-API Relay

Introduction

“Fluidize your AI subscription quotas and maximize the value of every penny.”

This is the No.73 article in the “One Open Source Project per Day” series. Today, we are exploring Sub2API (also known as CRS2).

With the rise of native AI power-tools like Claude Code and GitHub Copilot, many developers find themselves with multiple AI subscriptions (such as Claude Pro or OpenAI Plus). However, these subscriptions often come with usage limits or idle quotas. How can you consolidate these scattered subscription resources and distribute costs efficiently across different tools and users? Sub2API provides a perfect open-source solution.

It is more than just a simple forwarder; it is a full-featured API proxy platform that handles the entire pipeline — from account management and quota distribution to automated billing and built-in payments. It is particularly well-suited for team sharing, “carpooling” (shared-cost usage), or individual multi-account integration.

What You Will Learn

  • The core positioning of Sub2API and the pain points it solves.
  • Supported mainstream models and subscription types.
  • Core features: Multi-account management, intelligent scheduling, and Token-level billing.
  • Rapid deployment methods: Script installation and Docker Compose.
  • How to use Sub2API to build your own AI API relay service.

Prerequisites

  • Basic understanding of AI model APIs (OpenAI, Claude, Gemini, etc.).
  • Fundamental Linux command-line experience.
  • Familiarity with Docker or containerized deployment concepts.

Project Background

Project Overview

Sub2API is an AI API gateway platform developed in Go. Its core logic is to “pool” AI subscriptions from various channels (including OAuth-authenticated accounts, Session Keys, or standard API Keys).

With Sub2API, you can:

  1. Aggregate Resources: Plug in multiple Claude Pro or OpenAI accounts and output a single, unified standard API.
  2. Share Costs: Supports a “carpooling” mechanism with a built-in billing system to charge by usage.
  3. Seamless Integration: The generated APIs work flawlessly with native tools like Claude Code, OpenClaw, and more, without complex client-side configuration.

Author/Team

  • Maintained by: Wei-Shaw
  • Ecosystem: The project enjoys active community support, including a mobile admin console (sub2api-mobile) and other surrounding tools.

Project Data

  • 📄 Core Repository: Wei-Shaw/sub2api
  • 🛠️ Tech Stack: Go (Gin, Ent), Vue 3, PostgreSQL, Redis
  • 📄 License: LGPL-3.0
  • 📈 Stats: Over 22k Stars on GitHub (Note: May include historical repository data or high community interest).

Main Features

Core Utility

Sub2API solves the “resource island” problem in AI usage:

Upstream Account Pool (Claude, OpenAI, Gemini)
      ↓ Integration
Sub2API Platform Layer (Auth, Billing, Load Balancing, Session Persistence)
      ↓ Unified Distribution
Downstream Applications (IDEs, Chat clients, Scripts)

Key Features

  1. Multi-Account Management: Supports various upstream account types and automatically handles session persistence.
  2. Precise Billing: Token-level usage tracking and cost calculation with customizable rates.
  3. Smart Scheduling: Supports Sticky Sessions and load balancing to ensure continuity in long conversations.
  4. Built-in Payment System: Native support for Alipay, WeChat Pay, Stripe, etc., allowing users to top up autonomously.
  5. Concurrency & Rate Limiting: Configure per-user and per-account limits to protect your resources.
  6. Admin Dashboard: Provides an intuitive Web UI for real-time monitoring and management.

Quick Start

Method 1: One-Click Script Installation (Recommended)

Recommended for clean Ubuntu/Debian systems:

curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/install.sh | sudo bash

Notes:

  • Requires PostgreSQL 15+ and Redis 7+ to be pre-installed.
  • The script installs the binary to /opt/sub2api and creates a systemd service.

Method 2: Docker Compose Deployment

# Create deployment directory
mkdir -p sub2api-deploy && cd sub2api-deploy

# Download and run deployment preparation script
curl -sSL https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/deploy/docker-deploy.sh | bash

# Spin up services
docker compose up -d

Once installed, access the admin dashboard at http://YOUR_SERVER_IP:8080.

Detailed Analysis

Architecture: Why is this more than just a Reverse Proxy?

The design priority of Sub2API is “Account State Management.” Traditional reverse proxy tools (like Nginx) lack the ability to understand application-layer sessions.

  1. Sticky Sessions:
    For tools like Claude Code that require context continuity, Sub2API uses the session_id in the Header to lock a request to a specific upstream account, ensuring conversations aren’t interrupted by account switching.

Note: If using Nginx as a reverse proxy, ensure underscores_in_headers on; is enabled to support session headers.

  1. Pooling Logic:
    The system abstracts multiple accounts into a single “resource pool.” When one account hits a Rate Limit, the scheduler automatically routes traffic away from it, maximizing uptime.

  2. Integrated Ecosystem:
    While most relay tools require an external payment gateway, Sub2API’s built-in integration significantly reduces the operational complexity for small teams or community-led “carpools.”

Technical Stack

  • Backend: Go ensures high concurrency handling and ease of deployment.
  • Database: PostgreSQL handles complex relationships and billing records.
  • Cache: Redis manages rate limiting and real-time state synchronization.

Address and Resources

Official Resources

Target Audience

  • Carpool Leads: Organizers looking to split the cost of Claude/OpenAI Plus.
  • Developers / Power Users: Individuals wanting to consolidate multiple account quotas for native CLI tools.
  • Internal Enterprise Teams: Teams needing to distribute and audit AI resource usage internally.

Summary and Outlook

Key Takeaways

  1. Clear Focus: Specialized in converting subscription-based quotas into standard API services.
  2. All-in-One: A closed-loop for management, scheduling, billing, and payments.
  3. Deployment Friendly: Multiple options including script-based and Docker-based setups.
  4. Reliable Performance: Built on a solid Go/PostgreSQL/Redis foundation suitable for medium-to-large distribution.

One-Sentence Review

Sub2API is currently the most tightly integrated open-source solution combining “resource aggregation” with a “commercial model,” making it a powerful tool for achieving “subscription freedom.”

Visit my homepage for more useful insights and interesting products.

I Made the Wrong Bet on Event Streaming in Our Treasure Hunt Engine

The Problem We Were Actually Solving

In hindsight, we were trying to solve a multi-faceted problem that went beyond just event handling. We needed to ensure that the game state was always up-to-date, even when players were offline or experienced network latency. We also needed to prevent cheating by detecting and penalizing players who tried to manipulate the game state. On top of that, we had to ensure that the system could scale to handle thousands of concurrent players and millions of events per second.

We knew that event-driven architectures were the way to go, but we didn’t fully appreciate the tradeoffs involved in choosing the right event streaming platform.

What We Tried First (And Why It Failed)

Initially, we chose Apache Kafka as the event streaming platform, given its popularity and strong community support. However, we soon ran into issues with Kafka’s built-in limitations, such as high latency and limited topic partitioning capabilities. Our system was consistently experiencing 5-second lag, which compromised the overall gaming experience.

We tried to work around these issues by tweaking Kafka’s configuration parameters, but it was a losing battle. We also suffered from periodic crashes due to Kafka’s inability to handle high-throughput batch processing.

The Architecture Decision

After several weeks of tinkering, we decided to switch to a combination of Apache Pulsar and Redis. We created multiple event streams for different aspects of the game, such as player updates, game state changes, and chat messages. This allowed us to decouple each component and scale them independently, reducing the overall system latency.

We also implemented a Redis caching layer to store the game state, which reduced the load on our database and minimized the time spent retrieving data from the event streams. By using Redis as a caching layer, we were able to increase the time-to-live (TTL) of the cached data to 1 second, allowing us to provide the latest game state to players even when they experienced network latency.

What The Numbers Said After

Our switch to Apache Pulsar and Redis resulted in a significant reduction in system latency, from 5 seconds to under 50ms. Our event streaming platform was able to handle 10 million events per second, and our Redis caching layer was able to cache millions of game state records.

Perhaps more importantly, we saw a significant reduction in crashes and downtime, which was critical for maintaining the reliability of our system.

What I Would Do Differently

In retrospect, I would have taken the time to better understand the tradeoffs involved in choosing an event streaming platform. While Apache Kafka has its strengths, such as fault tolerance and high-throughput batch processing, it’s not the best choice for all applications.

I would have also explored alternative caching solutions, such as Memcached or Amazon ElastiCache, before settling on Redis. Additionally, I would have implemented a more robust testing framework to simulate high-throughput workloads and verify the performance of our event streaming platform under various conditions.

Ultimately, the key takeaway from this experience is that there is no one-size-fits-all solution for event-driven architectures, and careful consideration must be given to the specific requirements and constraints of each application.

#ai #productivity #chatgpt #python

At first, I thought AI coding tools were mainly competing on code generation quality.

But after building several AI-assisted projects, I noticed something more important:

The biggest bottleneck is no longer coding itself.

It’s context management.
Once projects grow larger, AI tools start behaving very differently.
What matters now is:

  • repo understanding
  • multi-file coordination
  • remembering dependencies
  • preserving architecture consistency
  • rollback safety
  • workflow orchestration
    In small demos, most tools feel impressive.
    But once my projects passed around 40+ files, the differences became much clearer.
    For example:
    Windsurf helped me move faster during:
  • rough prototyping
  • brainstorming
  • UI iteration
  • quick experimentation
    But Codex became much stronger for:
  • repo-wide cleanup
  • multi-file refactoring
  • dependency-aware edits
  • context-heavy modifications
    One thing I learned:
    The future AI coding battle probably won’t be:
    “Which model writes code better?”
    It will be:
    “Which system manages large-scale context better?”
    That changes how I evaluate AI tools now.
    I no longer judge them only by:
  • raw code output
  • benchmark scores
  • first impressions
    I pay much more attention to:
  • long-session stability
  • repo awareness
  • workflow continuity
  • architecture preservation
    AI coding is slowly becoming less about autocomplete…
    …and more about operating systems for development workflows.

Curious how other developers are experiencing this.

Symbolic Constant Conundrum

Introduction

A symbolic constant in any programming language is a name — a symbol — that can be used to stand in for a constant — a literal value. Programming languages inherited symbolic constants from mathematics that has many of them grouped by specific field of study. Examples include: π (pi), c (speed of light), e (Euler’s number), G (gravitational constant), h (Plank’s constant), etc. While those exact constants can be defined and used in programs, many programs define program-specific constants. Using constants is better than using magic numbers.

Both C and C++ have acquired multiple ways to specify symbolic constants as their respective languages have evolved over the decades, namely:

  1. Macros (via #define).
  2. Enumerations.
  3. const.
  4. constexpr.

Knowing which of the ways to use in a particular case can be quite the conundrum.

Macros

Originally, C only had macros, specifically, object-like macros, e.g.:

#define BUF_SIZE  8192

Macros are adequate, but not good. Why? Macros in general ignore scope, so you typically have to give macros very specific (long) names to avoid collision.

Enumerations

Enumerations in C and C++ are better, especially for declaring a set of related constants. In C++ with enum class, they can even be scoped to avoid collisions; in C, however, they’s still in the global scope.

The other caveat is that they can be constants only for integral values.

const

As I described for C and C++, you can use const for constants, e.g.:

static unsigned const BUF_SIZE = 8192;

char BUF[ BUF_SIZE ];

int main() {
  char local_buf[ BUF_SIZE ];
  // ...
}

In C++, that will compile just fine without warning; in C, it’ll either be accepted with warnings or rejected entirely, especially if you disable language extensions. Why? Because const is a misnomer since it really means immutable, not constant, and C is more picky about it.

While the declaration of BUF might be accepted, the declaration of local_buf will either be considered a variable length array (VLA) (that, as I pointed out, you should probably never use), or rejected since VLAs are an optional feature and not all compilers support them (notably, Microsoft’s C compiler doesn’t).

A common work-around in C (prior to C23, see below) is to (ab)use enum:

enum {
  BUF_SIZE = 8192
};

That is, use a nameless enumeration. The advantage is that enumeration constants really are constant.

constexpr

If you’re using C++11 or later, or C23 or later, there’s constexpr. Unlike const, constexpr really means constant. This is by far the best option for declaring constants:

constexpr unsigned BUF_SIZE = 8192;

Conclusion

To summarize:

  • If you’re declaring a set of related, integral constants, use enum (in C) or enum class (in C++).
  • Otherwise, use constexpr if you can.
  • Otherwise, use const.
  • Otherwise, use #define as a last resort.

From Manual RAG to Real Retrieval — Embedding-Based RAG with NVIDIA NIM

In Part 1, we built a USC campus assistant by pasting a five-line knowledge base directly into the prompt. That works when “the data” fits in your head. It stops being cute the moment the campus handbook, club docs, and workshop notes all want a seat at the same prompt window.

The fix is retrieval — store the chunks once, and at query time pull only the few that look relevant. That’s what RAG (Retrieval-Augmented Generation) actually means once you strip away the marketing.

This post takes the assistant from Part 1 and bolts on a real retriever, using NVIDIA’s hosted embedding model. No vector database, no LangChain, no abstraction layer. A Python list and NumPy are enough to understand what’s actually happening. Once you’ve seen the moving parts, swapping in pgvector or Pinecone later is a fifteen-minute job.

I’m B Torkian, NVIDIA Developer Champion at USC. Same workshop series, same campus, one more capability added.

What you’re adding

User question → embed query → compare to stored chunks → pick top-k → send only those to the LLM → answer

The model call itself barely changes. The work is in steps 2–4: turn text into vectors, compare vectors, return the closest chunks.

Why the manual approach from Part 1 breaks

In Part 1, the entire knowledge base sat inside the prompt:

campus_info = """
The USC AI Club meets every Thursday at 5 PM...
The USC GPU computing lab is open Monday to Friday...
...
"""

Five lines is fine. But every model has a context window, and every token costs money and latency. You don’t want to paste the entire USC student handbook into every question — most of it is irrelevant to “when does the AI Club meet?”

Retrieval is the answer to “which 3 paragraphs out of 3000 are actually about this question?” You compute that before calling the LLM, then send only the winners.

What an embedding actually is

An embedding is a list of numbers (a vector) that represents the meaning of a piece of text. Two texts that mean similar things land near each other in vector space. Two texts that mean different things land far apart.

NVIDIA’s nv-embedqa-e5-v5 is an embedding model tuned specifically for question-answer retrieval. It has a quirk worth knowing about up front — it treats queries and passages differently. You tell it which one you’re embedding via an input_type parameter. Getting this wrong is the most common beginner mistake — it still runs, but retrieval quality drops noticeably.

  • input_type='passage' → use for the documents you store
  • input_type='query' → use for the user’s question at search time

That’s it. Same model, two modes.

Step 1: Set up the client and ask() from Part 1

If you’re continuing from Part 1, you already have these defined and can skip this cell. If you’re starting fresh, paste this in first — everything later builds on it.

%pip install -q openai numpy

import os, getpass
from openai import OpenAI

if not os.getenv('NVIDIA_API_KEY'):
    os.environ['NVIDIA_API_KEY'] = getpass.getpass('Paste your NVIDIA API key (starts with nvapi-): ')

client = OpenAI(
    base_url='https://integrate.api.nvidia.com/v1',
    api_key=os.environ['NVIDIA_API_KEY'],
)

MODEL = 'meta/llama-3.1-8b-instruct'

def ask(system_prompt, user_message):
    response = client.chat.completions.create(
        model=MODEL,
        messages=[
            {'role': 'system', 'content': system_prompt},
            {'role': 'user',   'content': user_message},
        ],
        temperature=0.3,
        max_tokens=400,
    )
    return response.choices[0].message.content

client calls NVIDIA’s API Catalog. ask() is the same chat-completion shape from Part 1. The retriever we’re about to build slots in next to these, not instead of them.

Step 2: Build a small knowledge base and embed it as passages

import numpy as np

EMBED_MODEL = 'nvidia/nv-embedqa-e5-v5'

knowledge_base = [
    {'title': 'USC AI Club meeting',
     'text': 'The USC AI Club meets every Thursday at 5 PM in the engineering building, room 204.'},
    {'title': 'USC GPU lab hours',
     'text': 'The USC GPU computing lab is open Monday to Friday from 10 AM to 6 PM.'},
    {'title': 'NVIDIA Developer Program',
     'text': 'USC students can join the NVIDIA Developer Program for free.'},
    {'title': 'Next USC workshop',
     'text': 'The next USC AI Club workshop will cover Retrieval Augmented Generation (RAG).'},
    {'title': 'USC AI/ML office hours',
     'text': 'Office hours for the USC AI/ML faculty are Tuesdays 2-4 PM.'},
    {'title': 'USC robotics lab',
     'text': 'The USC robotics lab requires safety training before students can use the soldering station.'},
    {'title': 'USC tutoring',
     'text': 'Peer tutoring for introductory Python at USC is available Wednesdays from 1 PM to 3 PM.'},
]

def embed_texts(texts, input_type='passage'):
    response = client.embeddings.create(
        model=EMBED_MODEL,
        input=texts,
        extra_body={'input_type': input_type},
    )
    return [np.array(item.embedding, dtype=np.float32) for item in response.data]

# Embed every chunk once, as a passage. Store the vector alongside the text.
embeddings = embed_texts([item['text'] for item in knowledge_base], input_type='passage')
for item, embedding in zip(knowledge_base, embeddings):
    item['embedding'] = embedding

print(f'Embedded {len(knowledge_base)} chunks. Vector dim:', embeddings[0].shape[0])

Two things to notice:

  • The OpenAI Python client doesn’t have a native field for NVIDIA’s input_type, so we pass it through extra_body. That’s the right way to send provider-specific arguments without forking the client.
  • We’re storing the embeddings in plain Python dicts. For seven chunks this is fine. For seven thousand, you’d reach for a vector database (and the only thing that changes is where the vectors live; the cosine math is identical).

Step 3: Retrieve the top-k chunks for a question

def cosine_similarity(a, b):
    denominator = np.linalg.norm(a) * np.linalg.norm(b)
    if denominator == 0:
        return 0.0
    return float(np.dot(a, b) / denominator)

def retrieve_context(question, k=3):
    question_embedding = embed_texts([question], input_type='query')[0]

    scored = []
    for item in knowledge_base:
        score = cosine_similarity(question_embedding, item['embedding'])
        scored.append((score, item))

    scored.sort(key=lambda pair: pair[0], reverse=True)
    top_items = [item for score, item in scored[:k]]

    return 'n'.join(f"- {item['text']}" for item in top_items)

Three things are happening here:

  1. The question is embedded as a query, not a passage. This is the part beginners trip over. Same model, different mode.
  2. Cosine similarity scores how close the question vector is to each stored chunk vector. Numbers near 1.0 mean very similar; numbers near 0 mean unrelated.
  3. Top-k picks the highest-scoring chunks. Three is a reasonable default for a tiny knowledge base; tune it for yours.

There is no magic in step 3. A vector database would do the same comparison but use indexing tricks to do it fast at scale.

Step 4: Plug retrieval into the same ask() from Part 1

def ask_with_retrieval(question):
    context = retrieve_context(question)

    system_prompt = f"""You are a USC campus assistant. Answer ONLY using the
context below. If the answer is not in the context, say
"I don't have that information — check with the USC AI Club."

CONTEXT:
{context}
"""

    return ask(system_prompt, question)

for question in [
    'Where does the USC AI Club meet?',
    'When can I get Python tutoring at USC?',
    'What is the wifi password?',
]:
    print(f'Q: {question}')
    print(f'Context:n{retrieve_context(question)}')
    print(f'A: {ask_with_retrieval(question)}n')

Run it. Three things to read carefully:

  • The first question retrieves the AI Club chunk and answers from it. Good.
  • The second retrieves the tutoring chunk and answers from it. Notice that “Python tutoring” doesn’t appear verbatim in the stored text — the chunk says “introductory Python” — but the embedding model knows those are semantically close. That’s the whole point of vector search over keyword search.
  • The wifi question retrieves three chunks anyway (top-k always returns k items), but none of them contain a password. The assistant falls back to the refusal line because the ONLY using the context rule forces it to. That’s the guardrail from Part 1 doing its job — and it’s exactly the bridge into Part 3.

Step 5: What you actually did

You replaced the hand-picked campus_info string from Part 1 with a real retrieval step. The model call is identical, and the system prompt follows the same guardrail pattern — answer only from the provided context, otherwise fall back. The only structural change is that {context} now comes from a function instead of a hardcoded constant.

That swap is the entire mental model behind RAG. Real production systems add chunking strategies, hybrid search, re-ranking, and a vector database — but the spine stays the same: embed once, embed query, compare, pass top-k to the LLM.

In your own work, the seven-line knowledge_base becomes hundreds of paragraphs scraped from PDFs, lecture notes, club Slack archives, Notion pages, or a wiki. The retriever code doesn’t change. The dict-with-vector storage gets replaced by something like pgvector, Qdrant, or Pinecone the moment you outgrow a Python list.

Get the code

Repo: github.com/torkian/nvidia-nim-workshop
One-click Colab: Open the Part 1 notebook — paste the Part 2 cells underneath.

MIT licensed. I run this at USC — fork it, swap the knowledge base for your school, your club, your project, and run it wherever you are.

Previously / next in this series

  • Part 1: Build Your First AI App with NVIDIA NIM in 30 Minutes
  • Part 3 (next): Add Guardrails So It Doesn’t Lie — a two-layer approach using prompt scope + a tiny verifier call. The fallback line that fired on the wifi question above is the foundation we build on.

Building an outbound-only WebSocket bridge for local AI agents

Building an outbound-only WebSocket bridge for local AI agents

Building an outbound-only WebSocket bridge for local AI agents
I work with AI agents every day. Claude Code, Copilot, Gemini CLI — running locally, with access to my filesystem, my repos, my tools. The results are genuinely good. But there’s a wall: the moment you leave your desk, you lose control. There’s no real way to kick off an agent task from your phone, monitor a long-running pipeline from a coffee shop, or schedule something to run overnight.

Every solution I found had the same trade-off: you either open a port, install a tunnel daemon, or upload your code to someone’s cloud. None of those felt right for infrastructure that has access to your local filesystem.

So I built CTRL NODE — a browser-based control plane for local AI agents. The key piece is a process called the Bridge: a lightweight Node.js daemon that runs on your machine and connects to the cloud without ever accepting an inbound connection.

This article is about how that works, why the design choices matter, and what the actual code looks like.

Why outbound-only?

The naive approach is to expose your local agent runtime on a port and let the cloud reach in. Tools like ngrok do exactly this — they create a reverse proxy to your localhost. It works, but it has real costs:

  • Open port = attack surface. Every ngrok tunnel is a publicly reachable endpoint. If auth breaks, someone else can talk to your agent.
  • Third-party traffic relay. Your prompts, file paths, and agent responses travel through ngrok’s infrastructure.
  • Daemon complexity. You’re running persistent infrastructure that you didn’t write and can’t audit easily.

The alternative: flip the connection direction. The Bridge connects out to the cloud. The cloud pushes commands down that connection. The local machine never listens on a public port.

Your machine                     ctrlnode.ai cloud
──────────────────────────────────────────────────
Bridge ──── ws:// connect() ────▶ WebSocket server
       ◀─── {action: "run_task", ...} ────────────
       ─────  stdout/stderr events ──────────────▶

This is the same pattern used by IoT devices, CI agents (like the GitHub Actions runner), and remote desktop clients. The cloud doesn’t initiate — it waits.

The connection lifecycle

Here’s the core of websocket.ts:

export function connect(): void {
  const url = buildWsUrl();
  ws = new WebSocket(url, { headers: buildAuthHeaders() });

  ws.on("open", () => {
    logger.info("Bridge connected to SAAS");
    flushPendingQueue();
    startHeartbeat();
  });

  ws.on("message", (data: WebSocket.RawData) => {
    const message = JSON.parse(data.toString()) as InboundMessage;
    handleInboundMessage(message);
  });

  ws.on("close", (code: number, reason: Buffer) => {
    stopHeartbeat();
    if (isAuthError(code, reason.toString())) {
      logger.warn(`Auth error (${code}), retrying in ${AUTH_RETRY_MS / 1000}s`);
      setTimeout(connect, AUTH_RETRY_MS);
    } else {
      scheduleReconnect();
    }
  });

  ws.on("error", (err: Error) => {
    logger.error(`WebSocket error: ${err.message}`);
  });
}

Three things to notice:

  1. Auth errors get a longer timeout. If the server returns 1008 (Policy Violation) or 1002, or the reason string contains "401"/"403"/"Unauthorized", we wait 30 seconds before retrying. Hammering an auth-rejected endpoint is pointless and noisy.

  2. Normal closes trigger exponential backoff. scheduleReconnect() uses a standard backoff so a transient network blip doesn’t flood logs.

  3. On open, we flush the queue. More on this below.

Keeping the connection alive through load balancers

Cloud load balancers will kill idle WebSocket connections after 30–60 seconds. The fix is a heartbeat:

const HEARTBEAT_INTERVAL_MS = 20_000;
let heartbeatTimer: NodeJS.Timeout | null = null;

function startHeartbeat(): void {
  heartbeatTimer = setInterval(() => {
    sendToSaas({ type: "heartbeat", timestamp: Date.now() });
  }, HEARTBEAT_INTERVAL_MS);
}

function stopHeartbeat(): void {
  if (heartbeatTimer) {
    clearInterval(heartbeatTimer);
    heartbeatTimer = null;
  }
}

Every 20 seconds, a small message goes up. The server acknowledges it (or doesn’t — we don’t care, the goal is just to keep TCP active). This is cheap and it works reliably with AWS ALB, Cloudflare, and most managed WebSocket proxies.

Buffering outbound messages during disconnection

When the Bridge is reconnecting, agent output still arrives. If we drop those events, the user watching a pipeline in their browser sees a gap in the live log. The solution is a small in-memory queue:

const PENDING_QUEUE_MAX = 100;
const pendingQueue: OutboundMessage[] = [];

export function sendToSaas(message: OutboundMessage): void {
  if (!ws || ws.readyState !== WebSocket.OPEN) {
    if (pendingQueue.length < PENDING_QUEUE_MAX) {
      pendingQueue.push(message);
    }
    return;
  }
  ws.send(JSON.stringify(message));
}

function flushPendingQueue(): void {
  while (pendingQueue.length > 0) {
    const msg = pendingQueue.shift()!;
    ws!.send(JSON.stringify(msg));
  }
}

Cap at 100 messages, flush on reconnect. Simple, and it handles the common case of a 2–3 second reconnect window without losing events.

Multi-agent routing via the filesystem

Here’s the part that took the most thought: how do you run multiple agents — Claude, Copilot, Gemini — on the same machine, routing tasks to the right one?

The answer isn’t a routing layer in the WebSocket code. It’s the filesystem.

Each pipeline task gets an isolated directory:

workspace/
  tasks/
    task-abc123/
      input/
        TASK.md          ← instructions for the agent
        context-files/   ← any files the user attached
      output/
        TASK.md          ← agent writes progress here
        artifacts/       ← anything the agent produces

The Bridge watches these directories. When a run_task command arrives:

case "run_task": {
  const { taskId, agentProvider, workspacePath } = message.payload;
  const provider = getProvider(agentProvider); // Claude | Copilot | Gemini | ...
  await provider.executeTask(taskId, workspacePath);
  break;
}

Each provider implementation knows how to invoke its agent CLI with the right arguments and working directory. Claude Code gets claude --print with the task directory. Copilot gets its own invocation. They never share context — each runs in its own subprocess, reading from and writing to its own task folder.

This means:

  • No prompt pollution. Agent A’s context doesn’t leak into Agent B.
  • Parallel execution. Two agents can run simultaneously without coordination overhead.
  • Auditability. Every task leaves a paper trail on disk.
  • Portability. The cloud control plane never sees your file contents. It only sees task metadata and status events.

Provider selection and gating

Some actions only make sense for certain providers. The message handler maintains an explicit set:

const OPENCLAW_ONLY_ACTIONS = new Set([
  "openclaw_configure",
  "openclaw_stream_chunk",
  "openclaw_reset_context",
]);

function handleInboundMessage(message: InboundMessage): void {
  if (OPENCLAW_ONLY_ACTIONS.has(message.action) && activeProvider !== "openclaw") {
    logger.warn(`Received ${message.action} but provider is ${activeProvider} — ignoring`);
    return;
  }
  // ... dispatch to handler
}

This prevents misconfigured cloud deployments from accidentally sending the wrong command type to the wrong agent. The Bridge is the last line of defense before your filesystem.

The startup sequence

index.ts ties it together:

async function main(): Promise<void> {
  const providers = await createProviders(config);
  const multi = new MultiProvider(providers);

  connect(); // start WebSocket, non-blocking

  const keepaliveInterval = setInterval(() => {}, 1 << 30);
  keepaliveInterval.unref(); // don't prevent process exit

  process.on("SIGINT", gracefulShutdown);
  process.on("SIGTERM", gracefulShutdown);

  await multi.runSyncAgents(); // provider-specific background sync
}

The keepaliveInterval trick (unref()) is worth noting: it keeps the event loop alive when nothing else is pending, but doesn’t prevent a clean SIGINT/SIGTERM from shutting the process down. Without it, connect() is async and Node exits immediately after starting.

What this enables

With the Bridge running, the CTRL NODE web app can:

  • Launch tasks against any connected agent from any browser, anywhere
  • Watch live output streamed back over the same WebSocket
  • Schedule routines — the cloud scheduler wakes the Bridge at the configured time, no cron job needed on the local machine
  • Run multi-step pipelines where each node can use a different agent

None of your code leaves your machine. The cloud only sees: “task started”, “task output line”, “task completed”. The actual file contents, prompts, and agent context stay local.

Why open source?

The Bridge is MIT licensed (github.com/ctrlnode-ai/ctrlnode). You can read every line of the WebSocket handler, every message type, every auth check. If you don’t trust the binary, build it yourself.

The rest of CTRL NODE — the cloud scheduler, the web app, the real-time pipeline view — runs as a hosted service. The Bridge is the trust boundary: it’s the piece that runs with access to your local system, and it needs to be auditable.

Try it

If you work with AI agents and want a way to control them remotely without sacrificing privacy:

  1. Install the Bridge: npm install -g @ctrlnode/bridge && ctrlnode bridge start
  2. Sign up at ctrlnode.ai — it’s free
  3. Open the web app from anywhere and connect

Questions, issues, or PRs: github.com/ctrlnode-ai/ctrlnode or reply here.

Javier Vil — Creator of CTRL NODE

Our System’s Sins in Ghana: Why We Had to Rethink Digital Product Sales

The Problem We Were Actually Solving

At first glance, our platform was simply a successful e-commerce solution. But the truth was more complex. We had designed it to bypass local payment gateways, because they were either unreliable or outright forbidden in many countries, including Ghana. Instead, we routed payments through international gateways, which provided us with the scale and speed we needed. However, this made our platform inaccessible to a significant portion of our user base in Ghana. We were trying to solve the problem of scaling payments without considering the needs of the users in countries we were trying to include.

What We Tried First (And Why It Failed)

Initially, we tried to fix this issue by developing a separate plugin for Ghanaian payment gateways, but it was a failure. The plugin was complex to implement, and support was spotty at best. Users in Ghana often encountered a barrage of errors, from “card declined” to “payment failed.” It was clear we needed a different approach. We thought localizing payment options was the answer, but what we didn’t realize was that this would only address the symptoms of a deeper problem, not the root cause.

The Architecture Decision

After weeks of research, we finally made a breakthrough. We decided to integrate our platform with local payment providers directly, removing the need for international gateways altogether. This would not only resolve the issue but also improve our overall performance, as transactions would be processed locally and in real-time. We realized that our system’s design had been driven by the constraints of international payments rather than the needs of our users. By shifting our focus to local providers, we could tap into a more extensive network of payment options and improve our overall user experience.

What The Numbers Said After

After deploying the new architecture, we saw a significant uptick in sales from Ghana. The average transaction time dropped by over 70%, and the overall success rate for payments increased by 25%. These numbers told us that we had made the right decision. Our users in Ghana could now easily purchase digital products using their preferred payment methods. This change not only improved our user experience but also opened up new revenue streams for creators.

What I Would Do Differently

In retrospect, we should have considered the needs of our international users from the outset. We were so focused on scaling payments that we overlooked the consequences of our design choices on users in countries like Ghana. If I were to do it again, I would prioritize user experience and inclusivity much earlier in the development process. This would have saved us months of tinkering with workarounds and potentially prevented the disappointment of our users in Ghana.

Execution Governance, AI Drift, and the Security Paradox of Runtime Enforcement

Governance Paradox

Author: Michal Harcej | 23 May 2026

The next major battle in AI may not be model capability.

It may be execution governance.

As autonomous systems evolve beyond passive assistants into operational agents capable of making decisions, interacting with infrastructure, and executing actions in real environments, a deeper problem emerges:

How do we govern probabilistic intelligence under operational consequence?

Most current AI safety approaches remain largely:

  • policy-level,
  • observational,
  • post-hoc,
  • or moderation-oriented.

But increasingly, new architectures are attempting to move governance closer to execution itself.

This is where concepts such as:

  • runtime mediation,
  • hardware-anchored verification,
  • deterministic constraint enforcement,
  • semantic drift detection,
  • and execution assurance layers

begin entering the discussion.

The idea is simple in principle:

Instead of merely asking an AI system to behave safely, the system’s execution pathways themselves become governed.

In practical terms:

AI proposes action
↓
Governance layer validates admissibility
↓
Execution allowed, denied, quarantined, or escalated

This represents a shift from:
“trusting model behavior”

toward:

“verifying executable admissibility.”

The architectural direction is extremely important.

But it also introduces a serious paradox.

The deeper governance moves toward:

  • kernel layers,
  • hypervisors,
  • runtime mediation,
  • trusted execution,
  • hardware-rooted attestation,

the more privileged the governance layer itself becomes.

And historically, privileged infrastructure becomes the primary attack target.

Governance Paradox

Security engineering repeatedly demonstrates this pattern:

  • antivirus platforms became exploit surfaces,
  • hypervisors faced escape attacks,
  • identity providers became centralized compromise points,
  • firmware trust systems introduced new persistence vectors.

Execution governance systems may face the same challenge.

A runtime enforcement layer capable of:

  • validating execution,
  • constraining autonomy,
  • mediating actions,
  • or anchoring operational truth

also creates:

  • additional attack surface,
  • semantic manipulation opportunities,
  • synchronization vulnerabilities,
  • trust concentration,
  • and systemic dependency risk.

This becomes especially critical in systems relying on:

  • deterministic timing,
  • semantic validation,
  • distributed coordination,
  • or hardware-level trust assumptions.

Even more interesting is the rise of semantic governance itself.

Future systems may not merely validate permissions.
They may validate operational meaning.

This introduces entirely new categories of risk:

  • semantic drift,
  • governance erosion,
  • policy reinterpretation,
  • entropy escalation,
  • and adversarial admissibility manipulation.

At that point, governance is no longer simply cybersecurity.

It becomes:

  • operational systems theory,
  • bounded autonomy engineering,
  • admissibility architecture,
  • and execution consequence management.

This is why the future of governed intelligence may ultimately depend less on adding infinite monitoring layers and more on reducing operational entropy itself.

The deeper architectural question becomes:

Can intelligence systems be designed with fundamentally bounded admissible state spaces before runtime complexity becomes ungovernable?

That question may define the next era of AI infrastructure.

tauguard.ai

Small AI database questions can become big scans

A small question can become a big database scan when an AI agent writes the query.

“Show me customers at risk” sounds harmless.

But depending on schema context, the agent might join:

  • accounts
  • subscriptions
  • invoices
  • usage events
  • support tickets
  • notes
  • activity logs

Then it may retry when the first query does not answer the question.

For production MCP database servers, row limits should be treated as a safety boundary, not a UI preference.

Useful defaults:

  • preview first, not full export
  • aggregate before raw rows when possible
  • enforced row limits per tool/intent
  • page cursors for continuation
  • explicit “more rows exist” metadata
  • structured refusal for unbounded requests
  • audit logs with rows scanned and rows returned

Longer version: Row limits for AI database agents

The model should never summarize 50 preview rows as if it saw the whole database.