Why code mode
If you’re building your own agents, you’ll soon find the need to build a “code mode” for your agents to access tools and other capabilities. In this post, we’ll share our experience building code mode for Indent, and why it’s a strictly better way to give your agent controlled and safe access to the external world.
As an artificial coworker, Indent needs to connect to every integration and tool that everyone at a company uses.
Our own internal Indent needs to be able to read from our Notion docs, maintain our Linear tickets, triage incoming Datadog alerts, manage our GitHub, read and write to Attio, our CRM, draft emails and manage calendar, create and edit slides and docs, track customer requests in Slack and Pylon, send text messages — you get the idea.
Context windows fill up quickly, so you can’t naively represent integrations (MCPs, API specs, CLI tools) in the tool context you send to LLM providers.
// One schema per operation, on every turn, used or not.
const response = await anthropic.messages.create({
model,
tools: [
{ name: "linear__list_issues", input_schema: { /* ... */ } },
{ name: "linear__save_issue", input_schema: { /* ... */ } },
{ name: "datadog__search_logs", input_schema: { /* ... */ } },
// ... 600 more for this session's integrations. This doesn't scale.
],
messages,
});
Code mode lets agents call tools by writing code, instead of emitting tool call blocks and waiting for responses. This saves context tokens, lets agents more efficiently search for tools, makes it easy to solve for multiple installations of the same integrations with a standardized interface, and provides a straightforward mechanism to pass intermediate results between tools without chewing up context.
Some interactions with Indent last only minutes but some (long running tasks and threads) can last weeks or months, and range widely over the integrations available to an organization, which are likely to change over the lifetime of an Indent session. With code mode, we can easily add tools to an ongoing session on the fly. Progressive disclosure lets the agent support 1000s of individual “tools” while keeping context manageable.
Standard tool calling isn’t built to naively support multiple versions of the same tool from different MCPs. For example, Claude Code takes the approach of namespacing all MCP tools with mcp__<server>__<tool> and it supports a single OAuth login per URL.
At Indent we quickly ran into instances where effective multiplayer agent use requires support for multiple installations of MCPs. In a single session, we often need to use an organization-wide Linear integration that can act as Indent, sitting alongside a personal Linear integration that can read an individual user’s private docs and inbox, acting on their behalf.
Code mode also makes the agent much faster at cross-referencing information across disparate knowledge bases. For example, suppose we need to correlate a set of organization UUIDs in Datadog to a list of customers who ran into an error. Instead of burning tokens and context getting a list of IDs from one place and then repeating those IDs in another place, the agent writes code that drops the information it doesn’t need and joins across data sources natively — so the session doesn’t even see the intermediate UUIDs.
In the limit, organizational intelligence also requires a host of audit, access control, and proxy functionality that is difficult to enforce across disparate integrations and modes of tool use. Code mode execution is a natural chokepoint for access control, secrets and credential management, and logging.
Code mode in Indent
Different agent harnesses’ implementations of code mode vary pretty widely. We wrote our own, since the current versions didn’t quite meet all of our needs.
Implementation | Types | Tool search | SandboxThe isolated runtime the script itself executes in. It is separate from the agent's computer, which the script can only reach through the APIs it is given. | Output | Computer accessWhether a running script can use the agent's own machine, its shell and files, beyond integration calls. For Indent that is the computer every session gets: a cloud machine or your Mac. |
|---|---|---|---|---|---|
Indent | TypeScript, type-checked before execution | BM25F search; only names in prompt | QuickJS in WebAssembly, 64 MiB heap | Text, images, audio, video, PDFs; partial results | Real bash and files, Node-shaped |
Codex | JavaScript | Inline declarations; search behind a flag | V8 isolate | Text, images, audio; partial results | Only through its shell tool |
OpenCode | JavaScript subset | ~2k tokens inline, then search() | Custom interpreter, in-process | Text only; tool images passed through | None |
Executor | TypeScript, types stripped unchecked | On demand via tools.search() | QuickJS in WebAssembly, 64 MiB heap | Text only; emit() plus return value | None |
Cloudflare | JavaScript | None | Isolate, no network | Text only; return value plus logs | None |
Typed code mode
MCP services already expose a strongly typed JSON schema, which agents can use to figure out the semantics of the information they receive — and, more importantly, to fail fast when they make an error.
Instead of throwing away these types (like other implementations), we built a system to convert MCP types to clean TypeScript definitions so we could typecheck scripts before running them in our sandbox.
The same script without and with the type check. It has a typo in an argument name and reads a field that doesn't exist. Only the checked run catches both, before any call is made.
We wrap all of our code mode invocations in async functions, so we can support async/await; we compile incrementally to bring compilation times down; and we do some tricks to reduce context bloat due to typecheck failures.
Context efficient tool search
We want our tool search to use as few tokens as possible to get the agent the information it needs. Searches take an input string (a set of space-delimited keywords) and return a list of matching TypeScript snippets. We worked on optimizing both the inputs (to have better hit rates in the presence of multiple installations) and the output (to ensure we get all the information we need).
Our tool search uses BM25 to rank exact matches for function, integration or installation names first. We also check adjacent keywords, so “data dog” will find “datadog”. Parameters are also indexed, so a search like “issues by email” can find the function it needs even if the function name doesn’t include the string email.
Three loose queries, one hit each. The result is the declaration the next script is checked against, not a list of candidates.
We find that types make it easier for the agent to work with tools, so (unlike OpenCode or Executor) we always expose a full TypeScript snippet to the agent. To reduce bloat, we paginate results. And we allow the agent to drop descriptions from search results, so it can more efficiently work with large queries.
Handling multiple installations of a single integration
We model every integration as a map of installations with a consistent interface. For example, if you have a personal Linear account alongside your organization-wide Linear installation, Indent namespaces each installation under the integration, allowing it to separately call, e.g., MCP_Linear["jane-doe"].search_documents and MCP_Linear["acme"].search_documents.
This lets the agent easily see what installations are available, and the capabilities available through each installation.
// List my issues across every Linear installation, personal and org, at once.
const results = await Promise.all(
Object.values(MCP_Linear).map((linear) =>
linear.list_issues({ assignee: me }),
),
);
Letting the agent shape its output
Indent works seamlessly with images, PDFs, and videos, so we needed native support for non-text outputs in Code Mode. Inspired by Codex’s use of text(), image(), and audio() functions, our code mode exposes text() and media() functions which the agent uses to generate (and encode) the individual blocks of a tool result. The output of each function gets safely truncated and handled to guard against context bloat or output loss. (A naive implementation can easily lead to a runaway completion making a chat unusable.)
To enable safe parallel execution, if a function throws an error during code mode execution, we log the state of any concurrent promises. Then, when the agent recovers, it knows what has completed or is in an uncertain state. This lets the agent be more confident running functions in parallel, since when something crashes it doesn’t need to re-check every tool call to see what happened. (Codex is currently the only other code mode implementation that exposes partial results.)
The script builds its result as it runs. Back-to-back text() calls extend one text block, and each media() call adds its own. When a call fails, the blocks so far still come back, followed by the error and the calls that didn't complete.
Effective computer use
We want our agent to be able to use its computer during code mode execution (other implementations do not allow this). The agent can use its computer to write large MCP results to a file (perhaps only reading a summary), or to read the output of a previous step in order to provide input to another function invocation without bloating context.
Our first implementation wrapped our ordinary read, write, and edit tool calls. But when we tested computer use, we found the agent was coming up with workarounds to wrap the tool call output. We found that giving the agent something shaped more like the real Node filesystem APIs let it work better. It turns out that the automatic truncation and formatting we do to make tool call results look nice to the agent is actively detrimental in code mode, where the agent can shape its own results.
We ended up reworking all of our custom tools to give outputs that are closer to the TypeScript APIs we would expect in code than to a nicely formatted string. We trust the agent to make effective use of these results in code.
Building your own
If you’re building meaningful agentic tooling at a large organization, sooner or later we’re sure that you’re going to run into all of these issues and we think code mode is the best way forwards for your agents. We think it’s important to have a lively and open conversation about architecture, technology choices, and tradeoffs around these kinds of systems. If you’re building something similar, we’d love to hear from you. And, of course, if you’re considering buying organizational intelligence rather than building, we’d love to have you as a customer.