Tool wrapping¶
Turning a library function into a tool:
Tool.wrap(fn, ...)directly, a shared-state handle, or a bridging class?
Pick by what's actually missing — usually nothing is, and the bridging
class is boilerplate that re-declares a signature Tool.wrap already reads.
Decision tree¶
Does a plain function (or a small set of them) with type hints already do
the work, one call in, one JSON-safe result out?
→ Tool.wrap(fn, name="...")
# zero bridging code — LazyBridge reads the signature/docstring
# natively (mode="signature", the default)
Do several tools need the SAME loaded/expensive state (a big array, a
parsed document, a fitted model) without re-loading it on every call?
→ One tool loads it and returns a handle (a Store/depot key); every
other tool takes that handle as a parameter and reads the state back
by key.
# Share state via a KEY passed through the LLM, never the state
# itself — and never give each tool its own private loader for the
# same underlying data.
Does the raw function's result need capping/redacting for LLM context
(an unbounded list, a huge array, a secret) or a specific envelope /
provenance shape a caller depends on?
→ A thin wrapper IS justified — but wrap ONLY the missing concern (the
cap, the envelope) and call the library function unmodified inside
it. The wrapper's parameters should still mirror the library
function's, not invent a parallel shape.
Is the tool's schema already known ahead of time (an MCP tool catalogue,
an OpenAPI operation, a third-party registry) rather than introspectable
from a Python callable?
→ Tool.from_schema(name, description, parameters, func, ...)
# the canonical no-signature path — not a bridging class either.
None of the above — you're re-declaring the function's own parameter
list, restating its docstring as a new description=, and/or reloading
data the function (or a sibling tool) could already read from a shared
handle?
→ You're writing boilerplate. Delete the wrapper; call Tool.wrap(fn,
...) on the library function directly.
Quick reference¶
| Situation | Use |
|---|---|
| Plain function, self-contained | Tool.wrap(fn, name=...) |
| Several tools need one loaded resource | A shared store handle (a *_key parameter every tool reads by) |
| Output needs an LLM-context cap or an envelope/provenance shape | A thin wrapper — cap/envelope only, delegate the computation unchanged |
| Schema is already known (MCP/OpenAPI/registry) | Tool.from_schema(...) |
| Wrapper re-declares the function's own signature | Delete it; wrap directly |
Notes¶
- The signature is the schema.
Tool.wrap(fn, name=...)(mode="signature", the default) introspects type hints and the docstring — includingAnnotated[type, "description"]per-parameter docs — into the tool's JSON schema. A hand-written bridge method that re-types the same parameters and re-writes the same description is strictly worse: two places can drift, and only one of them is what the library actually does. - Share expensive state by key, not by tool. When two or more tools need the same loaded data (a returns matrix, a parsed corpus, a fitted model), the library function itself should accept a store-key parameter and read from a shared store — not each tool re-implementing its own loader that happens to fetch the same data. One loader tool, N consumer tools, one handle passed between them.
- A cap is not a re-implementation. Wrapping a function to bound its output for LLM context (e.g. "return the 250 most recent items, not all 10,000") is legitimate bridge code — but the wrapper should still call the unmodified library function and only touch the part of the result that needs bounding, not rebuild the whole computation.
- This applies one level up too, with a different verdict. An agent
that exists only to hold one
ToolProviderplus a tailored system prompt (a "specialist") is not the boilerplate this page warns about — the system prompt is real content a raw tool list doesn't carry. The distinction is: a specialist agent adds a prompt around existing tools; a bridging class re-implements the tools themselves. Avoid the latter, not the former.
See also¶
- Tool — schema modes (
signature/hybrid/llm) and when each is appropriate. - Everything is a tool — the composition philosophy this decision sits inside.
- LLM codegen contract — the terse Always/Never version of this rule.
- Worked example: LazyTools' regime detection
connector — every tool is a one-line
Tool.wrapof alazystatsfunction; one tool loads data into a shared depot under adata_keyhandle, and every fitting tool reads that same handle instead of loading its own copy.