Building Agents in a Language That Knows What an Agent Is
Most of an agent codebase is not the agent. It is the supporting code every developer writes from scratch, because the languages we use were never designed for agents.
Most of an agent codebase is not the agent. It is the supporting code every developer writes from scratch, because the languages we use were never designed for agents.
We built the exact same app twice. Once with FastAPI, SQLAlchemy, LangChain, React, TypeScript, Vite, and Bun - the state-of-the-art (SOTA) stack that a senior engineer would reach for today. Once in Jac. Same features, same UI, same AI-powered categorization, same persistence. Both QA-verified to behave identically.
The SOTA version is 11 files, 233 lines of application code. The Jac version is 1 file, 46 lines.
These are good tools. FastAPI is arguably the best Python API framework. SQLAlchemy is the most mature Python ORM. LangChain is the dominant LLM orchestration library. React and TypeScript need no introduction. We picked the best of each category on purpose - this isn't a comparison against straw men.
And yet, building with the best-in-class version of every tool still produces ~5x more application code across 11x more files than expressing the same idea in a language designed to handle the full stack. We're calling this the polyglot tax - the code you write not to solve your problem, but to make your tools talk to each other across language, runtime, and type-system boundaries. This article is a line-by-line anatomy of where that tax shows up and what it costs.
If you've used jac create --use client to scaffold a Jac full-stack project, you've seen jac-client-node and @jac-client/dev-deps in your jac.toml. They're npm meta-packages — packages that exist solely to declare a list of other packages as dependencies. The idea: one line in your config gives you React, Vite, TypeScript, and everything else you need.
Sounds clean. In practice, it's a trap. I think we should replace both meta-packages with direct dependency injection, and I want to make the case for why.
If you've ever worked with a graph database at scale, you've probably hit the N+1 query problem. It's one of those things that doesn't show up in development but absolutely kills you in production. We ran into it with Jac's tiered memory system and spent a week figuring out the right fix. This is the story of what we tried, what failed, and what actually worked.
It started in a Jaseci weekly sync. We were discussing the jac-mcp server when someone brought up the idea of an MCP playground — a place to test MCP servers the way Postman lets you test REST APIs. The idea stuck with me, but it didn't feel real until I ran into the problem myself.
I was connecting jac-mcp to GitHub Copilot, trying to understand how tools were being called and what they returned. The answer was: I had no idea. Copilot called tools based on whatever prompt and model it chose, and I was left guessing.
When I started building the jac-shadcn MCP server, I hit the same wall. Testing through an LLM client meant my results depended on the model and prompt, not the server itself. That's not testing. That's hoping.
Every browser automation script you've written has the same hidden bug: it doesn't know what it's doing. It knows the steps. It doesn't know the structure.
You write page.goto(), then page.fill(), then page.click(). Each call succeeds or fails on its own. The script has no idea that it just navigated to a login page, that the text field it filled was a username input, or that the button it clicked submits a form. It's running commands against a stateless API. The moment the page structure changes, a new modal shows up, an element gets renamed, there's an extra redirect, the script breaks. Not because you got the logic wrong. There just wasn't any logic. Just steps.
This is where browser automation sits in 2026. Playwright, Selenium, Browser-use, Stagehand, they all give you good primitives. None of them give you a model for what a browser session actually is. State lives in local variables. Context evaporates between function calls. The relationship between a session, its pages, and their elements exists only in your head.
I built jac-browser to try to fix this. It's a browser automation library written entirely in Jac, 153 walkers that wrap Playwright. But the wrapper isn't really the point. The point is that browser sessions are graphs, and Jac gives you graphs as a first-class language construct. The result is a library where the data model is the automation framework.
Every model demo you've ever watched -- Anthropic, OpenAI, whoever -- follows the same script. Someone types a request, the model produces something impressive, the audience applauds. It's a compelling showcase for the model. It's a terrible workflow for an engineer.
I've spent years in compilers, language runtimes, and distributed systems -- the kind of work where handwaving doesn't survive contact with reality. When AI coding assistants arrived, I fell into it like everyone else. The demos make it look so clean.
It took longer than I'd like to admit to recognize the pattern: it's the same thing you see when a developer copies from Stack Overflow without understanding the code. The demo format trains passivity. It optimizes for throughput at the expense of understanding. That tradeoff doesn't hold up in production.
Most programming paradigms ask you to learn dozens of concepts, patterns, and best practices. Object-Spatial Programming (OSP) is different. At its core, there are really just four fundamental concepts you need to understand. Master these four things, and you'll have unlocked an entirely new way of thinking about computation—one that's particularly well-suited for modern applications dealing with complex relationships, graph structures, and distributed data.
This article will teach you the complete model of Object-Spatial Programming through these four conceptual buckets. Each builds naturally on the last, and by the end, you'll understand why OSP represents a genuine paradigm shift in how we structure programs.
Python's traditional class syntax has a problem: defining any class with fields requires excessive boilerplate. After decades of developers writing the same __init__, __repr__, and __eq__ methods, Python 3.7 introduced dataclasses (PEP 557) as a decorator-based solution. But dataclasses are a retrofit—what if dataclass semantics were built into the language from the start?
When explaining complex software architectures, there's this tendency to start with the grand vision and work down to implementation details. But honestly, for developers who actually need to use a tool, the opposite approach makes way more sense. This post if for the Python developer exploring Jac, a programming language and runtime that extends Python rather than trying to replace it. We'll start with what you can actually install and use today, then work our way up to why it exists.