Redouble AI Nucleo documentation
Part 13 · Inside the runtime

Inside palettes and the tool registry

ai.redouble.nucleo.tools.registry · nucleo-core

This page is for people working on the runtime itself: the contracts local to the classes of ai.redouble.nucleo.tools.registry. The guide for building a thinker's palette is Palettes and the tool registry; palette assembly, the reconcile hooks and admission are described for authors in Tools, thinkers and doers.

The registry catalogs every tool the process ships (ToolProvider, ToolRegistry); a thinker draws its palette from it by declared compatibility (ToolSelector, SkillSelector); a palette bound read-only stays read-only (ReadOnlyPalette). A model may ask for more mid-run through two meta-tools (request_tools, request_skill), and the ask is judged like everything else: the same declarations and admission rules decide it, so a model can request what you offered and nothing else, and every grant is on the record.

ToolProvider: the single abstraction

ToolProvider carries everything the thinker pipeline needs to present and invoke a tool: name, description, JSON schema, weight, display metadata, input parsing, readOnly(), and a factory that builds the concrete Tool for a parent. metadata() is the open extension slot beside them - a host-defined provider carries tool information the framework's own surface does not name (icons, categories, cost hints); the framework reads nothing from it. Registries store providers, never classes.

readOnly() defaults to false because that is the correct answer for a provider that cannot know: a tool is treated as mutating unless something asserted otherwise.

Parse refusals are correctable: parseInput throws InvalidInputException, whose message names the field (from the mapping failure's own reference chain) and the declared type, never the value the model sent; the decoder's own complaint, which quotes that value, survives on the cause, where only a stack trace carries it.

ToolRegistry: names to providers

A registry maps ToolProvider.name() to the provider. The duplicate policy: a second registration under an existing name is an upsert when the new provider has the same concrete type and name (system-wide plus per-thinker overlap is harmless); a registration under an existing name with a DIFFERENT provider type throws IllegalStateException, so collisions surface early. Register and unregister are state-idempotent, which is what makes the per-turn reconcile hooks safe to run every turn. createTool on an unknown name throws a correctable error naming the available tools, so the model can pick another. A null name - a malformed call can omit one - is just an unknown name to every lookup that resolves a provider (getProviderByName, createTool, getInputTypeByToolName, getDisplayName, getActionVerb), rather than letting the backing map throw on a null key; hasTool and unregister(String) go to the map directly and do throw on a null name.

Class-taking conveniences (register(Class), registerAll, unregister(Class)) wrap the class in a ClassToolProvider; a class without @ToolName is skipped with a warning.

ReadOnlyPalette: the read-only binding, enforced twice

sweep removes every mutating provider from a registry and names each removal - it decides what the model is OFFERED. requireReadOnly asserts a tool may RUN, and is the guarantee: the registry is rebuilt every turn and can be widened after the definitions were built (by request_tools, by system-wide tools, by a direct addTool), so only the per-call check holds. Both are called from AbstractThinker's own call path, outside any overridable hook. request_tools is swept like any mutating tool (it admits arbitrary tools); request_skill survives (admitting a skill changes the conversation's own preamble and nothing outside it).

The meta-tools

RequestToolsTool and RequestSkillTool are resource-free doers a thinker injects itself into (ToolRegistryAware; the skill tool also ConversationAware). Each call goes to ToolHub for the reconciled catalog and lazy admission; admitted tools land in the thinker's registry and their schemas appear on the next call, admitted skills attach to the conversation and ride its preamble from then on. Rejections come back named with reasons in RequestToolsResult / RequestSkillResult. A name already present reads as admitted, not as an error.

Selectors

ToolSelector accumulates providers from classes, packages (via ToolHub package scans) and direct provider additions; a thinker returns one from declareCompatibleTools(). SkillSelector names skills by exact name, by bundle-id prefix, or everything the registry holds - resolved against SkillRegistry when getSkills() is called, which ToolHub does once per thinker class before caching the result, so a selector never has to name the skills a skilljar holds. A declared name the registry does not hold is logged and skipped; the palette must not go down with a missing bundle.

Tests

ReadOnlyPaletteTest (sweep, admission, defaults, the construction-time latch), RequestToolsSchemaTest / RequestSkillSchemaTest (the catalog-as-enum schema and its snake_case spelling), RequestSkillPaletteTest (the per-turn offer and the conversation landing), ToolHubSkillAdmissionTest (the admission door), SkillSelectorTest (resolution-time reads), ThinkerPaletteContractTest (registry idempotence, in tools.thinking).

^