Product

Code mode

How agent mode writes a short script to read, filter and join across systems in one step, why a script can only read, and where it runs.

Open Memtro

In agent mode, the model can write a short JavaScript function that calls Memtro's read tools, filters and joins what they return, and hands back only the result. One step instead of twenty, and the hundred records it read never enter the conversation: only the three lines that answered the question do.

The tool is memtro_run_script. There is nothing to switch on. It is offered on its own when a run has enough to work with, and the model decides when a script is the right shape for the work.

What it is for

Most investigations are not hard, they are long. "Who owns the accounts with an overdue invoice over £5,000?" is a billing list, a filter, and then a CRM lookup for each customer that survived it. Done as separate tool calls, every invoice and every CRM record goes through the model's context, and goes through again on every later step, because the conversation is re-read each time.

A script does the same work in one step:

async () => {
  const { invoices } = await tools.xero_invoices({ type: "ACCREC", overdue_only: true });
  const large = invoices.filter((i) => i.amount_due > 5000);
  const owners = await Promise.all(
    large.map((i) =>
      tools.hubspot_search({ object: "companies", query: i.contact, limit: 1 })
        .then((r) => r.results[0]?.hubspot_owner_id ?? null)
        .catch(() => null),
    ),
  );
  return large.map((i, n) => ({ invoice: i.number, customer: i.contact, due: i.amount_due, owner: owners[n] }));
}

What comes back to the model is the returned list, the calls the script made and how long each took, and anything it logged along the way. The invoices and CRM records it read stay inside the script.

Good fits are the mechanical parts of an investigation: list and filter, count, join two systems on an id, take the top few, look up the second thing keyed on the first one's result. When the sub-task needs judgement about what it finds (whether a thread was really resolved, what a customer was actually upset about), the agent hands it to a delegate instead, because a delegate reads the records and reports in its own words.

A read path, and only a read path

A script can call the tools that read. It cannot call anything that:

  • changes a system of record — any connector action: sending, posting, creating, updating, closing;
  • writes something that outlives the request — memtro_remember and the register tools;
  • starts a run of its own — memtro_delegate, jobs, or another script.

This is not a setting, and it is not on a list somebody maintains: a tool is withheld because it is marked as an action, a persistence tool or one that spawns a run, so a connector action added next year is withheld the day it ships. A script that tries one is told why in one line and carries on.

The reason is the provenance rule. Every value a script computes was computed from what a tool returned, so a script that read and then wrote would be acting on arguments that came from third-party content rather than from a person or a system of record, which is exactly what Memtro refuses for an action the model takes directly. So a write stays a step the agent takes itself, in the open, under its own grant and its own checks. The script is the thing that worked out what to write.

Every call is checked as if the model made it

A call inside a script goes through the same wrapper as a call the model makes as a step. Team reach, grants, the secrets check, the result cache, taint from third-party content: all of it runs, unchanged, because a script's tools.* functions look up the tools the run already holds and call those. There is no second path to the connectors.

What a script returns is treated as external data, the same as any connector result: sanitised again after the script has put its strings together, and wrapped as content rather than instruction. A run that read external content through a script is marked as having done so, exactly as it would be had it read it directly.

Where it runs

The script runs in QuickJS compiled to WebAssembly, inside Memtro's own process, with nothing bound into it but the tools. There is no process, no require, no fetch, no file system and no network. The usual escape from a JavaScript sandbox (this.constructor.constructor("return process")()) reaches QuickJS's own Function, which can only build more functions inside the same empty box. The sandbox is a property of the engine, not a list of things taken away.

Each script has a budget:

LimitValue
Tool calls100 per script
At once6 in flight
Per call15 seconds
Whole scriptUp to a minute, and never past what the request has left
Memory64 MB
What comes back24,000 characters; a script that returns more is told to summarise inside the script instead

One slow system cannot starve the rest, because each call has its own share of the clock. A script that throws returns nothing but its error, so the agent is told to wrap each call and return what it did get; anything it logged before the failure is still shown back.

When it is offered

  • Agent mode on Memtro's own loop. That is the Chat page by default, and any API request with "memtro": {"mode": "agent"} or a :agent model suffix.
  • When there are at least eight connector read tools in the run. Below that there is little to join, and a tool the model reads on every step without using is a cost for nothing.

It is not offered to:

  • Delegates. A worker already runs its own loop over the same tools; a script inside a worker inside a lead would be work nobody can read.
  • The subscription CLIs (claude-code/*, codex/*), which run in their own sandboxed container and reach Memtro over MCP.
  • MCP clients. A coding agent connected to Memtro's MCP server usually has code execution of its own, and can already script against the tools it is given there.

What the model is told

The model is not shown every tool's full definition. It gets the names of the tools a script may call, grouped by system, and fetches one tool's parameters with tools.__describe("name") when it needs them. tools.__names() lists what is bound.

That is the second saving. A company with twenty-odd connections has around 150 connector tools, at roughly 420 tokens of definition each, read before the model writes a word and again on every step. The names of the same tools are under 900 tokens.

Seeing what a script did

In the Chat page's activity trail a script shows as its size (⟨14 lines, 612 chars⟩) with the calls it made beside it, in order, each with whether it succeeded and how long it took. The API response's memtro.tools lists memtro_run_script like any other call.

What it does not do

  • It does not write, send or remember. Those are always the agent's own steps.
  • It does not reach anything the run could not. A script has exactly the tools, grants and connections the request already had, and fewer of them.
  • It is not a general code sandbox. There is no file system, no package import and no network beyond the bound tools. It exists to move glue between tool calls out of the context, not to run arbitrary computation.
  • It is not kept in full. The trail records what the script called, not its source. A full record of what ran belongs to the action ledger, which is planned.

Also worth reading

Where agents remember your company

Sign in, add a provider key, create an API key and point your tools at Memtro. It takes about five minutes.

7-day free trial · card on file, nothing charged until it ends · cancel any time