Skip to content

perf(tools): generate serializable tool metadata artifacts - #6153

Merged
waleedlatif1 merged 5 commits into
perf/tools-params-splitfrom
perf/tool-metadata-manifest
Aug 1, 2026
Merged

perf(tools): generate serializable tool metadata artifacts#6153
waleedlatif1 merged 5 commits into
perf/tools-params-splitfrom
perf/tool-metadata-manifest

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Stacked on #6152 (which is stacked on #6151). Bases retarget as each merges.

Adds the generator and the typed accessors. No consumer is rewired yet — that's the next PR, which is where the module-count win lands.

What and why

@/tools/registry is a ~9,000-line barrel over 4,366 tools. Each ToolConfig mixes plain data (params, outputs, name) with closures (request.headers, transformResponse, directExecution, postProcess), and those closures reach every integration's SDK client and parser. That is why reaching the barrel costs ~4,700 modules.

I audited every client-reachable caller: none needs a closure. They need outputs, params, or an existence check.

Two artifacts, not one

artifact contents size
tools/generated/tool-metadata.ts id → { name, description, version, params, oauth } 3.96 MB
tools/generated/tool-outputs.ts id → outputs 4.21 MB

outputs is over half the data and has a single consumer, so it gets its own module — callers needing only params never load it.

The .json trap (please don't "clean this up")

The data is a JSON string parsed at runtime, not an imported .json and not an object literal. With resolveJsonModule — enabled repo-wide — a .json import makes TypeScript infer a literal type for all 4,366 entries:

tsc --noEmit
baseline (before this PR) 12.6 s
with .json imports 8 m 07 s (38×)
with .json imports + ambient declare module 8 m 18 s (no help)
with string literals (this PR) 12.0 s

A generated object literal is the same inference work. A single string literal is one cheap token for both compiler and bundler, and JSON.parse beats evaluating the equivalent literal at runtime. The trade-off is that these files diff as one line — acceptable for a generated artifact nothing reads by eye and CI verifies wholesale. This is documented in the script and the skill.

Safety properties

  • The generator refuses to emit any function value. Shipping executable config to the client fails loudly rather than silently. hosting and schemaEnrichment are excluded on those grounds — both hold functions (hosting.enabled, pricing, enrichSchema) and are server-only.
  • Empty param entries are stripped. The registry has one — stt_deepgram_v2 — which crashes callers that read param.type while iterating. Worth a separate look: it's a latent data bug.
  • tool-metadata:check is wired into CI next to the other generated-contract gates.
  • Generated dir is biome-ignored (it exceeds the 1 MB limit and was being skipped with a notice on every commit anyway).

Test plan

  • tsc --noEmit 12.0s (baseline 12.6s — no regression)
  • 6 new tests in tools/metadata.test.ts
  • Verified the tests can fail: injected a null param into the artifact → contains no null param entries went red; restored → green. Worth noting I first wrote that assertion in a form that couldn't fail (the real registry entry is undefined, which JSON.stringify drops for free), so the guard's rationale is now documented accurately.
  • tool-metadata:check passes; regeneration is deterministic (sorted keys)
  • Runtime spot-check: 4,366 tools, gmail_send params/outputs resolve
  • biome clean

Skill

Adds tool-registry-boundary — which module to import for which need, the three non-obvious artifact properties, and how to verify an edge is actually cut. The canvas route reaches the registry through four redundant paths, so cutting one alone moves the module count by ~1; measure the route, not the file you edited.

@waleedlatif1
waleedlatif1 requested a review from a team as a code owner August 1, 2026 06:31
@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview Aug 1, 2026 6:07pm

Request Review

@cursor

cursor Bot commented Aug 1, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Large generated artifacts and a new contract boundary affect every tool change workflow, but runtime behavior is unchanged until callers are rewired; CI and tests enforce sync and serializability.

Overview
Introduces generated, serializable tool metadata so client-reachable code can read params, outputs, and existence checks without importing the full executable @/tools/registry barrel (and its ~4,700 transitive modules).

scripts/sync-tool-metadata.ts projects the registry into two artifacts: tools/generated/tool-metadata.ts (name, description, version, params, oauth) and tool-outputs.ts (outputs only). Data is stored as a single JSON string + JSON.parse, not .json imports or object literals, to avoid a measured 38× tsc regression. The generator refuses to emit functions, strips empty param entries, and is wired via tool-metadata:generate / tool-metadata:check in package.json and CI.

@/tools/metadata and @/tools/metadata-outputs expose typed accessors (getToolMetadata, getToolParams, hasToolMetadata, getToolOutputsMetadata) with Object.hasOwn guards for prototype pollution. metadata.test.ts validates coverage, known tools, and no null/function values in artifacts.

Docs and agent skills/commands now require regenerating metadata after registry changes and add the tool-registry-boundary skill (when to use metadata vs getTool). Consumers are not rewired in this PR — module-count wins land in the follow-up.

Reviewed by Cursor Bugbot for commit 3480aee. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds generated, serializable tool metadata and output artifacts plus typed accessors, synchronization tooling, tests, and a CI freshness gate. Major changes:

  • Projects registry fields into separate metadata and output artifacts to keep executable tool closures out of client-reachable graphs.
  • Adds lookup APIs for tool IDs, parameters, metadata, and output schemas.
  • Adds deterministic generation/check scripts, CI enforcement, tests, and boundary documentation.

Confidence Score: 4/5

The PR appears safe to merge, with two non-blocking hardening issues in special-key lookup behavior and the generator's bounded function scan.

Existing projected data is JSON-safe and no production consumer is rewired yet, while the remaining concerns affect special unknown IDs and future schemas nested beyond the scanner's limit.

Files Needing Attention: apps/sim/tools/metadata.ts, apps/sim/tools/metadata-outputs.ts, scripts/sync-tool-metadata.ts

Important Files Changed

Filename Overview
scripts/sync-tool-metadata.ts Adds deterministic registry projection and serialization, but its function-value guard has an arbitrary depth limit despite recursively nestable schemas.
apps/sim/tools/metadata.ts Adds lightweight metadata accessors; direct key lookup can return inherited Object.prototype properties for unknown special IDs.
apps/sim/tools/metadata-outputs.ts Adds the isolated output-schema accessor and shares the inherited-key lookup weakness.
apps/sim/tools/metadata.test.ts Covers artifact presence, representative lookups, null normalization, and top-level function exclusion.
.github/workflows/test-build.yml Adds the generated metadata synchronization check to CI.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Registry[Executable tool registry] --> Generator[sync-tool-metadata.ts]
  Generator --> Metadata[generated/tool-metadata.ts]
  Generator --> Outputs[generated/tool-outputs.ts]
  Metadata --> MetadataAPI[tools/metadata.ts]
  Outputs --> OutputsAPI[tools/metadata-outputs.ts]
  MetadataAPI --> FutureParams[Future params and existence consumers]
  OutputsAPI --> FutureInference[Future output inference consumer]
  CI[tool-metadata:check] --> Generator
Loading

Reviews (1): Last reviewed commit: "perf(tools): generate serializable tool ..." | Re-trigger Greptile

Comment thread apps/sim/tools/metadata.ts Outdated
Comment thread scripts/sync-tool-metadata.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 3480aee. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 3480aee. Configure here.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

Note on the 4/5 score above

That review ran at ~06:00 UTC against an earlier head; the branch is now at a later commit and Greptile has not re-run despite repeated triggers (it does not appear to re-review on force-push for these stacked bases). Flagging it so the stale score isn't read as an open concern.

Its stated reason — inherited-key lookups (getToolMetadata('constructor')) and the generator's depth-capped function scan — has since been fixed, with each fix verified rather than asserted. Every review thread on this PR is resolved, and the reasoning is in the thread replies.

The full Test and Build workflow was dispatched against the stack head and passed. Note that ci.yml only fires for PRs targeting main/staging/dev, so stacked PRs don't get it automatically — worth re-checking when each retargets to staging on merge.

Adds `scripts/sync-tool-metadata.ts`, which projects the executable tool
registry down to the data half nobody needs a closure for, plus typed accessors
over the result. No consumer is rewired yet — that is the next PR.

`@/tools/registry` is a ~9,000-line barrel over 4,366 tools. Each `ToolConfig`
mixes plain data (`params`, `outputs`, `name`) with closures (`request.headers`,
`transformResponse`, `directExecution`, `postProcess`), and those closures reach
every integration's SDK client and parser — which is why reaching the barrel
costs ~4,700 modules. Every client-reachable caller was audited: none of them
need a closure. They need `outputs`, `params`, or an existence check.

Two artifacts, not one. `outputs` is ~4 MB of the ~8 MB and has a single
consumer, so it is emitted separately and exposed from its own module; callers
needing only params never load it.

The data is a JSON string parsed at runtime rather than an imported `.json` or
an object literal. That is not stylistic — with `resolveJsonModule` (enabled
repo-wide) a `.json` import makes TypeScript infer a literal type for all 4,366
entries:

  tsc --noEmit, baseline                12.6s
  tsc --noEmit, with `.json` imports    8m07s   (38x)
  tsc --noEmit, with string literals    12.0s

An ambient `declare module` does not short-circuit it (measured: 8m18s), and an
object literal is the same inference work. A single string literal is one cheap
token for the compiler and the bundler, and `JSON.parse` beats evaluating the
equivalent literal at runtime.

The generator refuses to emit any function value, so shipping executable config
to the client fails loudly instead of silently. `hosting` and `schemaEnrichment`
are excluded on those grounds — both hold functions and are server-only.

Also strips empty param entries: the registry has one (`stt_deepgram_v2`, an
`undefined`) which crashes callers that read `param.type` while iterating.
`JSON.stringify` drops `undefined` on its own, so the guard is there for an
explicit `null` — which serializes faithfully and would reach consumers — and to
warn either way.

Wires `tool-metadata:check` into CI alongside the other generated-contract
gates, and ignores the generated directory in biome (it exceeds the 1 MB limit
and was being skipped with a notice on every commit).

Adds a `tool-registry-boundary` skill covering which module to import, the three
non-obvious properties of the artifacts, and how to verify an edge is actually
cut — the canvas route reaches the registry through four redundant paths, so
cutting one alone moves the module count by ~1.
Review found two real defects in the generated-metadata layer.

`JSON.parse` returns an object with the normal prototype, so a bare bracket
lookup resolved inherited members: `getToolMetadata('constructor')` returned a
*function* typed as `ToolMetadata`, and `getToolOutputsMetadata('toString')`
likewise — silently violating the accessors' documented "undefined if unknown"
contract. Guarded with `Object.hasOwn`, with a parameterised regression test
over `constructor`, `toString`, `valueOf`, `hasOwnProperty` and `__proto__`.

The generator's no-functions scan also gave up past ten levels of nesting. Param
and output schemas nest arbitrarily, so a deeper closure would have been dropped
silently by `JSON.stringify` while generation reported success — shipping an
incomplete schema and defeating the guarantee the scan exists to provide. The
depth cap is gone; a `WeakSet` handles the cycles that exposes.
A new tool now has a second registration step. Client code reads `params` and
`outputs` from the generated artifacts rather than from the registry, so a tool
added without regenerating them is registered but invisible to the UI — and CI
fails on the stale artifacts.

`add-tools` and `add-integration` are where someone actually adds a tool, so the
step goes in both, next to the registry edit and in each checklist.
Adding a block alone needs no regeneration — it references existing tool IDs and
changes no tool's shape. But a change that touches a tool alongside the block
does, and this is where that is easy to miss: a block's `outputs` are authored
to match its tools' outputs, and the UI now reads those from the generated
metadata, so a stale artifact makes the block's declared outputs disagree with
what the panel renders (and fails CI).

Completes the tool-authoring surface alongside add-tools and add-integration.
The three tool-authoring skills said to regenerate after adding or changing a
tool, but not after removing one. Removal is equally breaking and equally
guarded: deleting a tool from `tools/registry.ts` without regenerating fails
`tool-metadata:check` (verified — exit 1), so a contributor following the skill
literally would have hit a CI failure the skill never warned about.
@waleedlatif1
waleedlatif1 force-pushed the perf/tool-metadata-manifest branch from 7f543aa to 380ba1a Compare August 1, 2026 18:03
@waleedlatif1
waleedlatif1 merged commit d6e08d3 into staging Aug 1, 2026
25 checks passed
@waleedlatif1
waleedlatif1 deleted the perf/tool-metadata-manifest branch August 2, 2026 00:00
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