Back to News & Insights
Artificial Intelligence September 26, 2026 · 4 min read

How I built deferred tool discovery for my desktop AI assistant (no embeddings needed)

I'm building Ankita, an open-source desktop AI assistant (Electron + terminal CLI) that can run shell...

How I built deferred tool discovery for my desktop AI assistant (no embeddings needed)

I'm building Ankita, an open-source desktop AI assistant (Electron + terminal CLI) that can run shell commands, edit files with approval diffs, search the live web, manage scheduled routines, and talk hands-free. It's powered by GitHub Copilot models, and one design constraint has shaped almost everything in its architecture:

That constraint is really about context, not packaging. When your assistant calls websearch or gitdiff, the model needs the full parameter schema for every tool it might pick — and those schemas are context you pay for on every single request. Ship dozens of tools with everything loaded upfront and you burn tokens before the user has typed anything.

So I built a deferred tool-discovery system. Here's how it works, and the three small decisions that made it feel invisible.

Ankita's tool catalogue covers web search/fetch/scrape, Git, filesystem, process management, scheduling, page watches, GitHub notifications, project memory, MCP servers, image generation, voice… Each tool is one ESM module under tools/, exporting name, description, parameters, and run(). If I loaded all of them into every request, the model would see hundreds of lines of JSON schema before the conversation even starts.

The assistant's default toolset is deliberately small. When it needs something outside that set, it calls a single tool:

It takes a plain-language query — "search the web", "remind me daily", "where does this project stand" — and returns the matched tool schemas. Those tools then become callable in the same session. The rest stay unloaded.

The matching lives in tools/catalog.mjs. Tools are grouped into categories, and each category carries a short summary plus a keyword list:

matchCategories() in tools/find-tools.mjs then matches the user's query against category ids, keywords, and even tool names — with word-boundary regexes, not naive substring checks (so "port" doesn't fire on "transport"). It's pure and synchronous, which makes it trivially testable with Node's built-in test runner.

No embeddings, no vector index, no extra dependency. This is a deliberate trade-off: embeddings would handle paraphrase better, but a curated keyword list is predictable, debuggable, and costs nothing at runtime. For a desktop app where I control both ends, predictability wins.

There are also pragmatic disambiguation rules baked in — e.g. a "github notifications" query loads the built-in GitHub inbox category and not the connectors category, so one request doesn't pull in an unrelated schema. Real usage is full of these collisions, and a comment in the code documenting why beats cleverness every time.

Tools cover capabilities; skills cover procedures. A skill is a SKILL.md file with frontmatter (name, description, suggested-tools) and markdown instructions below. The skill tool loads one by name and returns the body, capped at 8000 characters:

Suggested tools are hints only — the skill never forces a tool call. This keeps procedural knowledge (like the repo's own conventions in ankita-dev: read before editing, zero runtime deps, run tests before reporting) out of the system prompt and loaded only when the task actually matches the skill's description.

The keyword lists are hand-maintained, and they drift: every time I add a tool I have to think about which synonyms users will type. I've considered generating candidate keywords at build time from tool descriptions and reviewing the diff — human curation with machine assistance, rather than either extreme. And the always-on categories (personal memory, skills) deserve a periodic review, because "always loaded" is a cost I should keep auditing.

Ankita is open source — akyourowngames/A.N.K.I.T.A — with portable Windows builds in every release. The code discussed here is in tools/catalog.mjs, tools/find-tools.mjs, and tools/skills/skill.mjs.

If you've built lazy tool-loading for an agent of your own — embeddings, keyword maps, something weirder — I'd genuinely like to hear what worked and what bit you. I'm 16, building this in public, and the feedback loop is the best part.

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation