Repository navigation
Letting a plugin be something the model can call #44
Closed
robert-dean
started this conversation in
Ideas
Replies: 1 comment
|
Closing as outdated. Every use case this listed (news, search, weather) shipped as its own capability instead, and One side note still holds: |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Written: 2026-08-09, while scoping the
llmseam.State of the tree: the tool loop itself is being built with the
llmcapability, host-side, overa registry that takes sources. This file is about adding a second kind of source. Today there is
one: catalog search, over
MusicProviderCatalog.searchTracks.News and RSS took a different route, 2026-08-15
Two of the three rows in the table below are built, and NOT as
toolplugins. News arrived as anewscapability with a host-side adapter (modules/llm/news.tool.ts) in front of it, which is thesame arrangement
chartshas. That is worth knowing before building the capability below, becauseit says something this file did not anticipate: a plugin that answers a QUESTION the station has is
better modelled as a capability, and
toolis for a plugin that wants to offer the model somethingthe host has no concept of. Weather is still the case that argues for it. See
news-and-bulletins.
And so did search, 2026-08-28
Built, and as a capability for the reason above rather than as a
toolplugin:searchin the SDK,SearchServiceinmodules/search,WebSearchToolin front of it,plugins/websearchbehind itover SearXNG, Brave or Tavily. Nothing about the
ToolRegistrychanged to accommodate it, whichis the thing the 2026-08-25 note below predicted and is worth having confirmed: a new source is one
registry.registerand one entry in the explicit list.Two things it settled that this file had left open.
"Is it a plugin?" is the wrong first question. "Is it a menu?" is the better one. Every source
here except this one answers out of a list somebody assembled — the library, the providers, a
published chart, the operator's own feeds. Search takes words the model made up and goes and asks,
and that is what makes it the last declaration offered and the one the prompt describes as a
fallback. The plugin/host split is a second question and is answered by the rule above.
A synthesized ANSWER is where the capability boundary actually bit. Tavily's
include_answerand SearXNG's infobox are the headline feature of both services, and neither has anywhere to land:
the capability has no field for one, the plugin turns the parameter off, and both parsers have a
test asserting the paragraph does not survive. A paragraph somebody else's model wrote about pages
this station never sees is a claim nothing here can check against a source — the same argument
SourceDocumentis built on, arriving from the opposite direction.And so did weather, 2026-08-29 — which leaves this file with no claimed user
Weather was the last row arguing for a
toolcapability, and it went the other way, on the rule thisfile states two sections up rather than on a new one: a plugin answering a question the STATION has is
a capability. Weather is that question twice over — the presenter asks it mid-sentence AND
station-moment's resolver wants a structured reading — so a
toolpluginwould have needed a second capability beside it to be useful at all.
Built as
weatherin the SDK,WeatherServiceinmodules/weather,WeatherToolin front of it,plugins/weatherbehind it over Open-Meteo, the US National Weather Service or OpenWeatherMap, and aweatherbreak kind in the director. Nothing about theToolRegistrychanged to accommodate it,which is now the second time that prediction has held.
So the honest state of this file is that the
toolcapability has no user. Not deferred pendingone: every candidate it was written for is built, and each was better as a capability. That is worth
saying plainly rather than leaving the table below to imply otherwise, because the next person to
reach for a plugin that offers the model something will be reaching for something genuinely new —
and the rule to apply is still the one above, not this file's original shape.
What it also settled, which the search note above half-predicted: the boundary bit again, and in the
same place. A weather service will happily sell a written forecast ("a pleasant afternoon with
scattered showers"), and the capability has nowhere to put one — for
SourceDocument's reason andTavily's
answer's. What crosses is measurements, and the sentence is the station's to write.The table below is kept as written. Every row in it is now built, and none of them as a
tool.The shape
A
toolcapability in the plugin SDK:describe()returns declarations: a name, a description the model reads to decide whether it wantsthis, and JSON Schema parameters.
execute(name, args)runs one and returns a result.Both ends are plain JSON, so this needs no exemption from the JSON-safe rule. That is worth
saying plainly, because the two capabilities beside it are exceptions:
speak()returns a realReadableStreamandhost.fetcha realResponse. Here nothing crosses but data. A tooldeclaration is a description of a function, never a function, and the host is what actually calls
anything.
It also means the boundary test costs nothing to satisfy: classify it with the payload types in
boundary.json.safe.tsand it stays there.What wants it
Things the DJ would ask about mid-sentence, that live behind somebody else's API:
All three are the inputs a bulletin is written from, which is the segment kind after a talk break.
What does NOT want it
Not everything worth calling is a plugin. The registry takes sources, and a plugin is one kind:
searchTracksis onmusic.providerand every provider plugin already implements it, so a "find me a song like this"tool is a host-side adapter over the installed provider, not a new plugin and not a new boundary.
is a function in the module, and routing that through a plugin would be a boundary crossing in a
circle.
The rule of thumb: a tool is a plugin when the thing it talks to is somebody else's service. When the
thing it talks to is deadair, it is a source the module registers directly.
Things to get right when it lands
llmloop holds the single model slot from thefirst call to the last, so a slow tool holds the model. Bound each execution separately from the
loop's step count, and make a tool that times out an answer the model can see rather than a failure
that kills the generation.
"that did not work"; handing it back as text lets the model carry on and say something true. A
PluginErroron a tool call should be caught by the loop and returned as a tool result.describe itself differently, and
PluginLifecycleManager.reinitPluginalready runs on every configwrite. Read declarations through the registry rather than snapshotting them at boot.
construction; anything that writes needs a deliberate decision about whether a model gets to do it
unattended, and this file is not making that decision.
The tool loop has a second caller now, 2026-08-25
Everything above was written when the only thing that drove tools was a break writer. The persona
STORY pass (
persona.story.pass.service.ts) is the second, and it changes what asearchcapabilitywould be worth here: it is the one pass on the station that runs with tools ON deliberately, because
a character's invented past is worth having only where it is grounded in records this station
actually holds. A
searchsource registered inToolRegistryreaches it with no change to thatfile.
Both of the following were re-read on 2026-08-28, when one was built. What happened to each:
The fence in
persona.story.model.tswas written for a pass that could only look at thisstation's own library. A pass that can search the open web can also state what it found, and the
rule it is holding — a record may be in a story, an event involving a real artist may not — gets
much harder to keep when the model has a source to cite. Everything that pass writes arrives
suggested, so the operator is still the check; that is what makes the risk survivable rather thanwhat makes it go away.
Answered with one more line in the fence. The existing rule already forbade what needed
forbidding, and read as a rule about INVENTION rather than about claiming: on a pass that could
only see this station's library, "may not claim" and "may not invent" were the same sentence,
because there was no way to state a true thing about an artist. The added line says the rule holds
for anything the model looked up, and says what a tool result is FOR here — choosing what a
presenter might have been listening to, never saying what an artist did.
Every tool the registry holds is offered to every caller.
LlmService.toolsForis all ornothing, so a search tool added for the story pass is also in front of the break writers, which
today deliberately run with
tools: false. That is fine as it stands and would stop being fine themoment something wanted tools for one caller and not another.
Still true, and still fine, and it is now the interesting constraint rather than a footnote.
The two passes that run with tools on are the story pass and the set generator, and neither is the
one that would most obviously benefit: a talk break is where a fact off the web would actually be
heard, and it runs without tools because a break must be fast and both records are already in its
prompt. So a per-caller tool list is the thing to build the day somebody wants a break writer that
can look something up, and building it is a smaller job than making the break writers fast enough
to take the whole registry.
Related
and weather feed.
packages/plugin-sdk/CLAUDE.md§ "Trust and egress" for why a plugin is trusted code, which iswhat makes
executerunning in the host realm unremarkable.Moved from
docs/todo/tool-plugins.md. The file as it last stood: https://github.com/robert-dean/deadair/blob/71e431d45ce7b35a189c7d226832ec4a6247e9dc/docs/todo/tool-plugins.mdAll reactions