Skip to content

Move integration tool descriptions out of Go and into YAML#197

Draft
arjansingh wants to merge 8 commits into
mainfrom
feat/tmpl-prompts
Draft

Move integration tool descriptions out of Go and into YAML#197
arjansingh wants to merge 8 commits into
mainfrom
feat/tmpl-prompts

Conversation

@arjansingh

@arjansingh arjansingh commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

Every integration's tool descriptions lived inline in Go, about 1,400 tools across the adapters. Editing the wording for one tool was a code change, reviewed next to dispatch maps, compaction specs, and everything else in the file. Prose was slow to write and the reviews were noisy.

This PR moves all of it into a tools.yaml file per adapter. Editing a description is now a content change you can review on its own, with no Go in the diff. The tool list the server hands to the model stays byte-for-byte the same as before.

What was built

tools.yaml per adapter. Each adapter's tool code is now a few lines that load a YAML file. Descriptions, parameters, and required flags all live in the YAML. Every adapter with a fixed tool set moved over.

A stricter parameter type. Parameters and required-ness used to be two separate collections, cross-referenced by string name. A typo in the required list compiled fine and quietly changed the tool's contract. Now each parameter carries its own required flag, so a parameter that is required but never declared is no longer something you can write down.

Prose moved for the two meta-tools too. The long search and execute descriptions that teach the model how to discover and call tools moved into markdown files, same idea: code owns behavior, content owns the words.

A loud loader. A misspelled key fails at startup instead of quietly shipping a tool with a blank description.

The wire lock

The thing that makes this safe is a lock file on the tool list the server hands the model. It works like go.sum. The tools.yaml files are the source of truth, and server/tools_list.lock.json is the generated, committed snapshot of the exact wire output they produce. A test, TestToolsList_MatchesWireLock, compares the live output against the lock byte-for-byte on every run, and CI runs it on every PR.

That is what lets you trust this migration. Across all the adapters, the model sees the same tool list it saw before. Nothing was dropped or reworded by accident.

It also stays useful after the migration. The lock catches any future change to what the model sees, intended or not: a dropped description, a renamed parameter, a flipped required flag all turn the test red. When the change is intentional, you regenerate the lock and commit it with the YAML:

go test ./server -run TestToolsList_MatchesWireLock -update

You don't hand-edit the lock, the same way you don't hand-edit go.sum. An accidental change shows up as a lock diff in review instead of shipping quietly.

Design decisions

We kept the wire output identical on purpose. Nothing downstream of the server had to change, and the lock holds us to it.

Adapters that build their tools at runtime from a remote server keep doing that. Only the ones with a fixed tool set moved to YAML.

Required-ness now lives on each parameter. That also fixed a real bug: when search lifts a shared parameter into a shared block, it no longer loses whether a given tool needed it.

Test plan

  • make build, go vet, and the full go test -race suite green
  • wire lock test passes across all adapters, and -update regenerates it cleanly
  • CI lint and security (run on the PR)

Note

This branch was rebased onto the latest main, which had added twelve adapters since it diverged (the Google Workspace suite, Kubernetes, and Vercel). Those are migrated to YAML here too, so the whole adapter set lands on one shape. There is also a small unrelated fix in here for health probes hanging on an unreachable integration.

Co-Authored-By: claude-opus-4-7 noreply@anthropic.com

arjansingh and others added 7 commits June 25, 2026 22:32
The two meta-tools, search and execute, carry long descriptions that teach the
model how to find and call integration tools. Those descriptions used to live
inside the Go source. Every wording change looked like a code change in review, and
needed a rebuild to ship.

We moved the words into markdown files, one per tool. Now the code owns behavior and
the markdown owns the words. Reworking how we explain a tool to the model is a
content edit you can review on its own.

We tightened the wording while it moved. The model is told to ask before guessing a
tool name. Every integration's main tool opens with the same "start here" hint, so
they all sit at the same discoverability bar. And we pushed back on the mistakes
models make most: calling execute before searching, and guessing argument shapes
instead of reading the schema.

Co-Authored-By: claude-opus-4-7 <noreply@anthropic.com>
…equired

A tool's parameters and which ones were required used to live in two separate
places: a map of parameter names to descriptions, and a separate list of the names
that were required.

Nothing tied the two together. A typo in the required list, like "querry" instead of
"query", compiled fine and quietly changed the tool's contract. You could even mark a
parameter required that did not exist.

We folded the two into one. Each parameter now carries its own required flag, so a
parameter that is required but never declared is no longer something you can write
down.

All 36 adapters moved to the new shape with the same parameters and required flags as
before. Nothing the server hands out changed, and a snapshot test holds us to that.

Co-Authored-By: claude-opus-4-7 <noreply@anthropic.com>
Every integration's tools were written inline in Go, about 1,400 of them across 36
adapters. Editing the wording for one tool meant a code change, reviewed next to
dispatch maps and everything else in the file. Writing prose was slow and the reviews
were noisy.

We moved all of it into a YAML file per adapter. Each adapter's tool code is now a few
lines that load the YAML. Editing a description is a content edit with no Go in the
way.

A misspelled key fails at startup instead of quietly shipping a tool with a blank
description. The few adapters that build their tools on the fly from a remote server
keep doing that; only the ones with a fixed tool set moved.

Nothing the server hands out changed. The same snapshot test from the reshape holds
the tool list identical across all 36 adapters.

Co-Authored-By: claude-opus-4-7 <noreply@anthropic.com>
When the same required parameter showed up across several tools, search would lift it
into a shared section to avoid repeating it. But lifting it lost the fact that it was
required. The model saw the parameter listed once with no sign that a tool actually
needed it, so it would leave it out and the call would fail.

Now each tool remembers which parameters were required, and search reports from that
memory instead of working it out again after the parameter has been moved. Lifting a
parameter into the shared block can no longer erase that it was required.

We also stopped merging a parameter into the shared block when tools disagree about
whether it is required. If owner is required in some tools and optional in others, it
stays on each tool, since one shared entry can only tell one of those stories.

While we were here, the loader now rejects any tool or parameter with an empty
description, instead of shipping a blank one that only surfaces later as a model
failure.

Co-Authored-By: claude-opus-4-7 <noreply@anthropic.com>
… search extraction

Readability passes on a few helpers, no behavior change.

A couple of functions in the YAML loader and the search code nested their real work
inside a positive check. We flipped those to return early on the empty case, so the
main path stays at the left margin instead of indented.

One helper had two separate guards that did the same thing for a missing value and a
null value. We merged them, so "nothing here means no parameters" reads as a single
decision.

Co-Authored-By: claude-opus-4-7 <noreply@anthropic.com>
Listing integrations or checking their health could hang forever. The check asked
each integration in turn whether it was healthy, with no time limit on any one
question. A single unreachable integration, like ollama on a host that never answers,
would stall the whole thing behind it.

We gave each check a two second budget and now run them all at once instead of one
after another. Worst case, these tools take about one budget no matter how many
integrations are down, instead of adding up every slow one. If the caller hangs up,
the checks stop early.

This is a stopgap. The real fix is to let an integration say why it is unhealthy, not
just whether it is. Right now a timeout, a refused connection, and a bad token all
look the same: unhealthy, no reason, go read the logs. Letting the health check return
an error the way our other calls already do would put the real reason on the wire. We
left that for later.

We also wrote down a testing rule: a stale binary running an old build looks exactly
like the new build, so any test against a live server has to confirm it is hitting the
right version before trusting the result.

Co-Authored-By: claude-opus-4-7 <noreply@anthropic.com>
…tools to YAML

We moved every adapter's tool descriptions out of Go and into tools.yaml.
Twelve adapters that landed on main after this branch started never made
that move, so once the branch caught up to main, the build broke.

Those twelve still declared their tools the old way. The Google Workspace
suite (Calendar, Chat, Docs, Drive, Forms, Meet, People, Sheets, Slides,
Tasks), Kubernetes, and Vercel each wrote their parameters as a Go map with
a separate list of required names. The migration had already deleted the
type that shape relied on.

This commit moves all twelve to tools.yaml and the three-line loader, the
same as every other adapter. We checked each one against its original Go
definitions, tool for tool and field for field, before removing that check.

A few existing adapters also grew tools on main while we were away.
ClickHouse added connection listing, Cloudflare added a large batch (AI
Gateway, Workers AI, Vectorize, Queues, Hyperdrive, Zero Trust, and more),
and GitHub added runner registration tokens. The code to run those tools
came across in the rebase, but their descriptions did not, so we add them
back to each adapter's YAML. ClickHouse also needed one dispatch entry put
back.

Last, we register the new adapters in the wire-identity test and regenerate
its fixture, so the test now covers the full set of tools the server ships.
The fixture only gained entries. Nothing already there changed.

Co-Authored-By: claude-opus-4-8 <noreply@anthropic.com>

@acmacalister acmacalister left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI all green (build, test, lint, security). PR moves 1,400+ tool descriptions to YAML with byte-identity snapshot test and no Must Fix or Should Fix issues found. LGTM.

…date flag

The byte-identity fixture was named for the migration that created it,
tools_list_pre_migration.json, and lived under testdata like throwaway test
scaffolding. It is neither anymore. It locks the exact tool list the server
hands the model, the same idea as go.sum: a generated file, committed, that
CI fails on when the real output drifts.

We renamed it to tools_list.lock.json and moved it beside the server package
it locks. We renamed the test to TestToolsList_MatchesWireLock and gave it an
-update flag. Changing a description is now edit the YAML, run the test with
-update, commit both. Without -update the test still fails on any drift, which
is the point. An accidental change to what the model sees shows up in review
instead of shipping quietly.

CI already checks this. The race-test step runs the whole tree on every PR, so
the lock is enforced without a dedicated step.

Co-Authored-By: claude-opus-4-8 <noreply@anthropic.com>

@acmacalister acmacalister left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean PR. All CI checks passed (build, test, lint, security). The YAML migration is thorough, the wire lock enforces parity, and the architecture patterns are followed correctly. LGTM.

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.

2 participants