Why Agent Harnesses Keep Coming Back to TypeScript
I run an AI discussion group. If you want to learn about Agent project implementation or exchange ideas about Agent technology, you can add my WeChat
yunmz777.
When it comes to AI development, Python is often the first thing that comes to mind. But look at coding agents like Claude Code, Gemini CLI, OpenCode, and Pi, and TypeScript appears with striking frequency.
The "harness" here is the program that actually runs the task outside the model. It manages sessions, organizes context, calls tools, and also handles user interruptions, execution results, and task recovery. The agents we see in terminals or editors depend on these capabilities.
I think that when discussing this phenomenon, we should first lay out the daily work of developers. Once the model provides capabilities through the API, building products around it involves a lot of familiar application-development problems, and TS happens to tie quite a few of them together.
There are other implementation routes for harnesses, such as Codex having moved to Rust. So what I want to talk about is: why do so many teams choose TS, when is this convenience worth keeping, and when do you need to switch to something else.
What a harness does every day
Suppose we ask an agent to modify a project. It needs to find the relevant files, hand the necessary content to the model, then check the parameters of the tool calls returned by the model, execute the operations, and send the results back. During this process, the interface needs to keep showing progress, and the user may change the requirements or press stop at any time.
Once you actually start, you hit more details: a command fails halfway through, a file has been changed but the session hasn't been saved, the user closes the window and reopens it, a tool call never returns.
These are all problems a harness has to deal with. Its core loop can be very short, but the execution and state management built around it cannot be omitted.
For products that primarily call remote models, a lot of time is spent waiting on network, files, databases, and tool processes. Node.js's event loop and non-blocking I/O are well suited to organizing this kind of work. TS provides type checking; the actual async operations are handled by runtimes like Node.js and Bun.
Python's machine-learning ecosystem is strong, but once the model is provided by a remote service, we may not need to run training- or inference-related libraries in the harness. More often we're dealing with requests, events, tools, and UI. During language selection, how much weight these tasks carry naturally becomes important.
In a related Reddit discussion, developers noted that much agent work is actually API calls, and product developers tend to stick with the tech stack they already know.
Async capability is not unique to TS. Python's asyncio is also suitable for concurrent I/O; the GIL should not be understood as Python being unable to wait for multiple network requests at once. What's worth comparing here is development habits and the surrounding ecosystem.
A single feature often goes from the runtime all the way to the UI
Once you've built a chat interface with tool calling, you'll notice that many features can't be changed in just one place.
Say we want to add file-modification approval. The tool execution layer has to generate an approval request, the UI has to show the change, and only after the user confirms can the runtime continue. The timeline also needs to record this operation, and when the session is restored, it needs to know whether it was completed.
A seemingly small feature can involve tool execution, message protocol, state storage, and UI.
If all this code lives in a TS project, we can share event types and runtime validation rules. When a field changes, related call sites can be checked together, and the state displayed in the frontend is easier to keep consistent with the runtime.
This is very attractive to teams already using React and Node.js, because we don't have to maintain two sets of interface definitions or build a separate codegen pipeline just to bridge two languages.
The runtime environment of the product also influences the choice: VS Code's local and remote extension hosts use Node.js, the Electron main process also has a Node.js environment, and the UI uses web technologies. Writing TS on top of that is usually quite smooth.
Terminal products can also reuse these lessons. For example, Gemini CLI's official engineering notes list TypeScript, React, and Ink, and split the terminal UI from model orchestration into separate packages.
I think the fact that several layers of a feature can be modified together is a very practical advantage of TS. Unified language reduces coordination cost, but module and process boundaries still need careful design.
Types and tool parameters can be maintained together
In a harness, there are many pieces of data that look similar but actually cannot be used interchangeably. Text deltas and tool results need different fields, waiting for approval and failed execution correspond to different handling, and tools that read files versus modify files have their own parameter requirements.
TS's union types, generics, and type inference are good at describing these relationships. But for agents, static types alone aren't enough.
The data returned by the model comes from outside the system. Just because we've written an interface in code doesn't mean the JSON the model actually returns matches it. So TS often needs to be paired with Zod, TypeBox, or other runtime validation solutions.
For example, a parameter definition for reading a file could look like this:
import { z } from "zod";
const ReadFileInputSchema = z.object({
path: z.string(),
startLine: z.number().int().positive().optional(),
});
type ReadFileInput = z.infer<typeof ReadFileInputSchema>;
// The parameters returned by the model must be validated at runtime
const input = ReadFileInputSchema.parse(modelOutput);
This way we can both derive the type in code from the schema and check the parameters passed in at runtime. In SDKs that support such schemas, we can also use it to describe tools to the model. The Tool Calling documentation of the AI SDK shows exactly this combination.
The benefit is concrete: when adding a parameter to a tool, it's easier to maintain the description the model sees, the data received by the execution function, and the related call sites together.
It's worth noting that writing as ReadFileInput doesn't validate data. TypeScript types are removed at runtime, so external input still needs to be actually checked.
Passing validation doesn't mean the operation is necessarily sensible either. For example, a path being a string doesn't guarantee it's inside an allowed directory. Tool permissions, execution conditions, and result checks still have to be the program's responsibility.
What I value here is the convenience of combining types with runtime contracts. It reduces interface inconsistencies but does not guarantee that every step an agent takes is correct.
Existing libraries and plugins influence the final choice
Part of language selection is plain and simple: how much of what we lack do we have to build ourselves?
In another Reddit discussion, a developer who had long used Kotlin explained that he finally chose TS because it's widely applicable and has enough existing libraries. If he'd insisted on his original language, he'd have had to build quite a bit himself.
When building a harness, we need to integrate model SDKs, handle streaming output, manage child processes, and possibly connect to browsers, editors, and various external services. For teams familiar with TS, how much they can pull from existing projects directly affects development cost.
Plugins are similar. Some people want to check parameters before a tool call, others want to add custom tools. If the extension mechanism is close to what they already know, it's easier to start.
OpenCode's plugin mechanism supports local JS and TS files, as well as npm packages. Plugin functions receive context and return hooks to register, then attach to the corresponding events.
Such mechanisms also create compounding effects. The more existing extensions there are, the easier it is for later people to keep developing along the same technical path.
Other languages aren't locked out by protocol. The MCP official SDK list includes official support for Python, Go, Rust, and others. TS's appeal is more about whether existing tools fit smoothly into your project.
The Pi team, in their introduction to Pi Durable, directly answered why they continue to choose TypeScript:
Because it is the easiest way to bootstrap this.
Their explanation is direct: using TS to bootstrap this system is the least effort, which also says a lot about the priorities in real-world selection.
Claude Code also considered what the model is familiar with
When building developer tools, we usually consider what the team is familiar with. The Claude Code team also considered one more thing: what the model participating in development is familiar with.
In the team interview from September 2025, Boris Cherny explained that they want to use a tech stack that Claude already knows well, so that Claude Code can directly participate in developing itself. TypeScript and React were technologies that models were particularly good at at the time.
When we frequently have a coding agent modify a project, whether the tech stack is common, whether error feedback is clear, and whether changes are easy to verify all affect the actual experience. Type checking provides a portion of that clear feedback, letting the model keep correcting interface or parameter errors.
However, this was only the basis for the Claude Code team's choice at that time; you can't infer from it that TS is the best fit for every model or every project.
For us, the more useful question is: can the model in hand reliably make changes in this project, and can we promptly notice when it changes something wrongly?
Codex's shift to Rust has its own solid reasons
After all this, another question naturally arises: since TS is so convenient, why did Codex switch to Rust?
In its official migration announcement from May 2025, Codex explained the reasons directly:
- Reduce installation dependencies and avoid requiring users to pre-install a specific version of Node.js
- Use native security and sandbox bindings more directly
- Lower runtime and memory overhead
- Continue to support other language extensions through protocols
The same announcement also affirmed React and Ink, saying the early terminal UI was very easy to set up and iterate.
These two things are not contradictory. During development, you need to quickly build interactions and verify functionality; but when software is to be delivered to more users, installation, resident memory, and system capabilities become more prominent.
GitHub later published the migration of the Copilot Agent Runtime with similar considerations. After moving the core from TypeScript, Node.js, and V8 to Rust, the team gained more control over resource overhead and enabled applications in other languages to choose in-process embedding. What was migrated is the core runtime; the UI and per-language SDKs are separate layers.
Even if model responses require waiting, the cost of local software is still worth optimizing, because the memory an acceptable session uses can become a problem when many sessions run simultaneously.
TS software can also be turned into standalone programs. Bun's executable packaging delivers the code along with the runtime, so users don't need to install the corresponding environment first. However, ordinary packaging still includes a JS runtime; improving distribution doesn't mean all runtime overhead disappears.
I would look at installation, startup, memory, concurrency, and system interfaces separately, rather than only at which language the project ended up being written in.
If we were the ones building it
If the product already uses Electron, React, and Node.js, I'd first build the main portion of the harness in TS, because sessions, model adapters, tool protocols, event streams, and UI often need to be modified together. Reusing the existing stack lets us spend more time on whether these behaviors are correct.
At the same time, I'd clearly separate the boundaries of CPU-intensive computation and external tool execution, arranging appropriate execution locations for large file parsing, complex indexing, and long-running commands. Adding async to a function doesn't automatically move CPU computation to another thread. The Node.js documentation explicitly warns to avoid blocking the event loop with CPU-intensive work.
Whether to switch languages still depends on what most affects the product right now: if the problems are in tool parameters, context organization, task cancellation, or session recovery, fix those first. If you're clearly stuck on startup, memory, system interfaces, or cross-language embedding, then consider a native implementation.
For us, the main reason to choose TypeScript is to be able to complete interactions, tools, and agent flows with one familiar set of technologies, and then add underlying capabilities as needed.