Skip to content
5 min read · 937 words

Skills Battery

This is a featured battery

Skills are plugins. This section documents the lifecycle, source seam, context channels, capability boundaries, artifacts, scripts, middleware, and authoring format.

A skill is a plugin, and this is a plugin system. The manifest is plugin metadata, the body is operational guidance, tools are exported capabilities, the isolation battery is the sandbox, the gate is the host permission boundary, and the registry is the registration surface. load_skill is activation. unload_skill is deactivation. refresh_skills is upgrade/reload. SkillSource is the repository/loader seam.

A skill is to an LLM agent what a plugin is to WordPress. The vocabulary is not decorative. It tells you where teardown belongs and why source code is rewrapped at the host boundary.

Skill concepts mapped to plugin concepts

The mapping is useful when integrating an existing plugin host: the names differ, but the lifecycle responsibilities do not.

Skill conceptPlugin concept
ManifestPlugin metadata
BodyPlugin-provided guidance
ToolsExported capabilities
Isolation batteryThe sandbox
GateHost permission boundary
RegistryPlugin registration surface
load_skillActivation
unload_skillDeactivation
refresh_skillsUpgrade/reload
ManagerPlugin lifecycle manager
SkillSourcePlugin repository/loader seam

We are about to state an opinion

Plugin systems figured lifecycle out decades ago; agent skills somehow forgot the second half.

The industry built plugin discovery and plugin activation, then inexplicably stopped. Every plugin system that predates this one shipped teardown in its first API:

  • WordPress has register_activation_hook and register_deactivation_hook (and register_uninstall_hook).
  • VS Code extensions have activate() and deactivate().
  • OSGi's BundleActivator defines start(BundleContext) and stop(BundleContext).

agentskills.io describes Discovery → Activation → Execution. There is no fourth stage. ADK has one because a lifecycle without an exit is not a lifecycle; it is a hostage situation with documentation.

The five lifecycle tools

forgeSkillTools returns five Tool instances. They are ordinary registered tools, so the deployment still chooses placement and gating.

ToolJob
list_skillsShow discovered routing metadata. refresh: true refreshes the catalog; query filters id, name, or description.
list_loaded_skillsShow skills projected into this dispatch, plus manager-initialized ids.
refresh_skillsExplicitly rediscover and replace a loaded version in the current dispatch.
load_skillRead the body, project it, and register rewrapped tools.
unload_skillRemove the body projection and unregister skill-owned tools.
ts
import { createSkillManager, forgeSkillTools } from '@nhtio/adk/batteries/skills'
import type { SkillSource } from '@nhtio/adk/batteries/skills'

const source: SkillSource = /* your repository */ null as never
const manager = await createSkillManager({
  sources: [source],
  gate: async () => undefined,
})
const lifecycleTools = forgeSkillTools(manager, { gate: async () => undefined })
// Register lifecycleTools with the same host registry as your other tools.

Two meanings of “loaded”

Initialized means the manager has a live descriptor, body retrievable, wrappers, and (when configured) workspace. manager.loaded() answers that manager-level question.

Projected means those objects are in this particular turn or dispatch context. manager.projected(ctx) answers what is callable here. The turn registry is rebuilt every turn, so initialization does not magically survive into a fresh registry. middleware.turnInput performs that projection.

That distinction is the reason unload works. ADK reassembles context per dispatch instead of appending skill bodies to an immutable conversation log. Deactivation removes the projection; the next assembly simply does not contain it. autoRefresh only updates catalog knowledge. It never silently swaps a loaded version; refresh_skills does.

One manager per conversation/session/assembly. Managers are cheap; share source objects if discovery caching matters, but do not share the manager's loaded state across tenants.

Deactivation is not amnesia

Unload removes the skill body from future context. It does not erase the bytes already written to your spool store, and it does not erase a tool result the model already extracted.

That is the design, not a shortfall. Deactivating a WordPress plugin does not roll back the rows it wrote. Uninstalling a compiler does not un-compile your binary. A lifecycle that destroyed the work done while the plugin was active would be useless — you could never deactivate anything you had actually used.

So the two halves come apart cleanly, and that separation is the feature:

  • The instruction set is reclaimable. The prose that steered the model — the expensive, permanently-resident part everywhere else — leaves the moment you deactivate.
  • The work is durable. Findings, extracted sections, script output, spooled bytes: all ordinary tool results, living by ordinary tool-result rules, entirely independent of whether the skill that produced them is still loaded.

Reclaiming context without discarding results is the whole point. An agent can load a skill, use it, unload it, and keep everything it learned — then load the next one against a context that is not carrying the last one's manual.

A reader forged before unload stays callable for the rest of that iteration and can still read its durable bytes; core prunes and re-forges readers from the next iteration's retrievables. One iteration, closing on its own — and, consistently with the above, it reads bytes that were always meant to outlive the skill.

The honest limits

There are no memory, CPU, or process limits supplied by this battery. Script timeouts have a ceiling, but an in-process tool handler that never settles hangs the turn. This is a plugin lifecycle, not a magical operating system.