Skip to content

feat: Defer Tools, opt-in defer_loading for tools sent to Meridian - #1

Merged
jaedync merged 3 commits into
mainfrom
feat/defer-tools
Nov 13, 2026
Merged

jaedync merged 3 commits into
mainfrom
feat/defer-tools

Conversation

@jaedync

@jaedync jaedync commented Nov 13, 2026

Copy link
Copy Markdown
Owner

Adds Defer Tools, an opt-in extension that marks a configured list of tools defer_loading: true in Anthropic requests that pi sends to Meridian. Anthropic tool search then loads them on demand, so their definitions leave the prompt.

What it does

  • Hook: before_provider_request. Changes only requests to Meridian (configured base URL, or the x-meridian-agent request header). Other providers are unchanged.
  • The list is frozen per session and stored in the session (defer-tools/frozen entry). Resume, fork and /reload use the stored list. A session with earlier turns and no entry stays off. A config edit reaches only new sessions, so a running session never changes its tool list.
  • Never defers read, bash, edit, write, codemode, powershell, and never defers every tool.
  • A deferred tool loses cache_control (the API rejects both keys; Meridian does not forward client cache_control), so pi's cache retention setting cannot change the deferred set.
  • Loads before Cache Compaction, so its captured and warmed payloads carry the same marks.
  • Off by default. Setting: deferTools.names in pi-extras.json. Kill switch: PI_EXTRAS_DEFER_TOOLS=0.
  • Needs Meridian 1.89.0 or newer (upstream #1368 and #1370). Older builds send deferred tools in full and add a digest turn.

Test plan

  • node --test tests/defer-tools.test.ts: 30 pass (config, freeze, session entry on resume/fork/reload, Meridian detection, never-defer set, all-deferred guard, cache_control, byte-identical output, no input mutation, extension order).
  • npm test: 0 fail. npm run typecheck, npm run audit:package, npm run test:install, npm run lint:slop: pass.
  • Real pi 1.5.2 through a test Meridian with a real login: a deferred test tool was loaded by tool search, pi ran it, the model used its result. Passed on a Meridian 1.89.0 dist.
  • Real pi, 5 processes on one session (pi -c): one stored entry, 99 to 100% cache hits on every turn.
  • Raw pi-shaped requests, 31 tools, 13 deferred: first-turn prompt 36,944 -> 31,953 tokens (-13.5%); the turn after a load reads the whole earlier prefix from cache.

…Meridian

Defer Tools adds `defer_loading: true` to the tools named in
`deferTools.names` (pi-extras.json), in each `before_provider_request`
payload that goes to Meridian. A request goes to Meridian when the selected
model uses `anthropic-messages`, the payload's model id is that model's id,
and the model has a Meridian base URL (`deferTools.meridianBaseUrls`, default
127.0.0.1:3456 and localhost:3456) or its request headers carried
`x-meridian-agent` (seen in `before_provider_headers`; Pi 1.5.2 resolves
provider headers at request time, so `ctx.model.headers` is empty). Other
providers get the payload unchanged.

It is off by default: with any tool deferred, Meridian 1.89.0 lifts its
single-turn cap and every tool-call turn pays an extra hidden model turn.
The measured 13-tool list is exported as RECOMMENDED_DEFER_TOOLS and given
in the README.

Each session freezes its list at its start in a Symbol.for slot, so a
config edit or /reload never changes the tools of a running session. The
transform is pure: same payload, same bytes; tool and key order kept; the
input is not mutated. It never defers read, bash, edit, write, codemode or
powershell, keeps an existing defer_loading value, leaves a tool with
cache_control loaded (Anthropic rejects cache_control on a deferred tool;
Pi puts it on the last tool), and changes nothing when no tool would stay
loaded. PI_EXTRAS_DEFER_TOOLS=0 turns it off.

It loads second, after Tab Status and before Status Plus and Cache
Compaction, so Cache Compaction captures the deferred payload for its warm
requests. Pi's own cache warmer replays through onPayload, so it gets the
same deferral. Subagent children do not load it.
A resumed session read pi-extras.json again, so a config edit between two
runs changed its tools and rewrote its prompt cache. Now a new session
writes its frozen list once, at its start, as a `defer-tools/frozen` custom
entry (pi.appendEntry), and only when the list is not empty. A resumed,
forked or reloaded session takes the latest such entry on its branch
(sessionManager.getBranch) and ignores pi-extras.json. A session with an
assistant turn and no entry freezes to an empty list, so deferral never
starts in the middle of a session. Only a session with no turns reads the
config.
…e key

Pi puts cache_control on the last tool only while cache retention is not
`none`, so skipping such a tool tied the deferred set to that setting, and
a retention change in a session changed what Meridian saw. A listed tool is
now deferred whether or not it has cache_control, and its cache_control key
is removed (Anthropic rejects both on one tool; Meridian rebuilds tools
through MCP and never forwards a client's tool cache_control). The deferred
set depends only on the frozen list and the tools present. The guard that
keeps one tool loaded stays.

The docs no longer say that every tool-call turn pays a digest turn:
Meridian 1.89.0 keeps tool search without it (upstream #1368 and #1370);
older builds send deferred tools in full and add the turn. Default stays
off.
@jaedync
jaedync merged commit 86ca949 into main Nov 13, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant