Engineering notesView as Markdown ↗

An orientation payload an agent can actually read

The tool every fresh agent is told to call first had grown to ~111,000 characters — past the point where a client will inline a tool result. The orientation tool became the one thing an agent could not read. Default to a lean response with counts and pointers, gate the exhaustive form behind `verbose: true`, and assert the size in a test.

If your protocol tells every new agent "call this tool first," that tool's response size is part of your interface contract. An orientation payload that a client will not inline is worse than a thin one: the agent either truncates it, spills it to a file it may not be able to re-read, or proceeds unoriented.

That is what happened here. A session-context tool — the documented first call, carrying tenant identity, persona, conventions and a live state snapshot — measured 111,619 characters on a modest tenant. Roughly 90,000 of those were a single field: the full per-tool catalogue, inlined on every call.

Why it grows without anyone deciding to grow it

Nothing about that field was wrong when it was added. A categorised map of every callable tool is genuinely useful orientation, and at forty tools it cost little. The catalogue then grew past two hundred tools, each with a name, category and one-line summary. The payload grew with it, linearly and invisibly, because no test asserted a size and no reviewer sees a response's byte count in a diff.

The failure only appears at the boundary — in a client's tool-result limit, not in the server. So the server's own tests stayed green while the tool became unusable in exactly the situation it was designed for: a context-free agent on its first call.

There was a second, sharper detail. The tool already had a verbose parameter, documented as "reserved for future use." Its lean mode had been designed and never built. Anyone hitting the size limit would read the parameter list, pass verbose: false or omit it, and receive the same enormous response — the documented remedy was inert. A reserved flag on a shipped interface is worse than no flag: it looks like the answer.

The shape that fixes it

Default to the shape of the thing and pointers to expand it; gate the exhaustive listing behind an explicit opt-in.

// default — orientation, not inventory
{
  "tenant":   { "id": "…", "business_name": "…", "timezone": "…" },
  "user":     { "persona": "operator" },
  "conventions": { /* the handful of rules that prevent mistakes */ },
  "operational_state": { "active_clients": 33, "jobs_this_week": 30 },
  "tool_categories": { "clients": 13, "contractors": 12, "jobs": 4, "invoices": 6 },
  "deep_dives": [ "search_tools(query) finds a tool by intent", "…" ]
}

tool_categories — category to count — tells the agent the toolkit's shape in a few dozen characters where the full map took ninety thousand. It answers the only question orientation needs to answer ("is there a jobs surface here, and roughly how deep?"), and the follow-up is a different call: intent search for a specific tool, or the flat catalogue for the authoritative list. Long prose fields get a cap and a pointer rather than truncation-by-accident.

verbose: true returns the full map, unchanged, for the caller that genuinely wants inventory — a documentation generator, say, rather than an operator's assistant.

Assert the budget

Size is a contract, so test it like one:

const res = await call({});                       // default shape
expect(JSON.stringify(res).length).toBeLessThan(20_000);
expect(res).not.toHaveProperty("tool_map");        // the balloon stays out of the default
expect(res).toHaveProperty("tool_categories");     // …replaced by its shape
expect((await call({ verbose: true }))).toHaveProperty("tool_map");   // detail still reachable

Seed the fixture tenant with enough data that the assertion means something — a hundred-plus jobs and a full contractor roster — otherwise it passes on an empty tenant and tells you nothing.

Transferable rules

  1. A response size is an interface constraint. If a documented "call this first" tool can exceed a client's inline limit, it has a bug regardless of its content.
  2. Default to shape, opt in to inventory. Counts and pointers beat exhaustive lists for orientation; the exhaustive list stays one explicit call away.
  3. Never ship a reserved parameter. Either implement it or remove it — a flag that silently does nothing is a false remedy.
  4. Cap free-text fields with an explicit "truncated, ask for verbose" marker rather than letting a client cut them mid-sentence.
  5. Test the ceiling on realistic data. Byte counts do not appear in code review; they appear in assertions.

Related

Last updated 2026-07-30