← AI Foundation

Tool catalog

ai-foundation · workzone
Schema forecast. The home of the tools table is this module's data model; the agent-selection link (agent_tools) lives in Agent Engine. Below is a concept, not an API contract.

A tool is a callable function the platform exposes to the model as a tool schema. The catalog of tools is owned by Admin.

Model calls by name
──▶ exposed as a tool schema
Tool name · params · body
──▶ result into context
Answer model continues
The catalog governs the set — what's enabled · seen by chat · allowed to agents Execution — the shared tool-calling harness, not here
Tool versus prompt → Prompt copy

Distinct things; don't conflate them.

Tool function
  • name · params · body
  • supplied as a structured tool schema
  • model calls by name → result into context
Prompt text
  • an instruction on what to do and how
  • steers behavior, isn't called

A tool schema comes from a structured source, not from admin prose — the call contract is reliable and won't break on a text edit.

Where a tool comes from — the type registry → Connector registry

The catalog is open; the mechanism is a type registry, mirroring the Harvester connector registry: a type declares itself as a class (name · tool schema · credential kind · body) and self-registers.

type — a class in code, self-registering  ──▶  instance — a row in tools (flags + key)

preset v1
Built-in read-only platform classes: web_search, fetch_url.
custom v1
The organization's own class following the same contract — as a pip package or in-tree, in the custom/ folder (custom tool).
mcp · openapi v2
An external self-describing server, no code.

Provider behind an interface. web_search is one tool to the model (web_search(query)), while the provider (Tavily · Brave · Serper · Google CSE) is chosen by the admin on a screen, not in a config file: the choice lands in the non-secret config column, the key in credential_enc. The model sees one stable schema; how it's executed is the adapter's business. The same technique used for ai_models providers and the built-in embedder.

A tool's secret (API key) is not in config (JSONB) but in a separate credential_enc column: AES-256-GCM ciphertext from Auth's crypto core, exposed outward only as a "set" flag. Like provider and SMTP keys. The human-readable name goes through t(); the DB holds only the key.

Custom tool — in code → Custom connector

The organization adds its own tool as one class following the contract: the body is a call method, the tool schema is in the manifest, registration via a decorator.

# where to put your class — two paths
✦ canonical
  pip package  entry point  achilles.tools
  core untouched · platform upgrade stays clean

○ quick dev
  in-tree  achilles/ai_foundation/tools/custom/
  own folder · doesn't disturb the built-ins
  1. 1 subclass · call
  2. 2 manifest · schema
  3. 3 @register_tool
web_search.py
# 3 · discovery picks it up itself
@register_tool
class WebSearchTool(BaseTool):       # contract: schema + call

    manifest = ToolManifest(             # defines what the model sees
        name       = "web_search",       # key + name in the tool schema
        params     = {"query": str},     # arguments for the model
        access     = ReadOnly,           # read_only · write
        config     = [Provider],         # Tavily · Brave · Serper · CSE
        credential = [ApiKey],           # → credential_enc
    )

    def call(self, args):               # the tool body
        return provider.search(args.query)

    def probe(self):                     # "Test" button → status
        return provider.ping()           # active · error
#  either way below — in-tree or as a pip package — the class is visible
#   only after rebuilding the image and redeploying the container:
#   discovery reads classes at process start, not from Admin
achilles/ai_foundation/tools/ · in-tree
├── base.py        BaseTool — contract: manifest + call
├── registry.py    discovery: package + entry points
├── web_search.py  ┐ built-in
├── fetch_url.py   ┘ presets (read-only)
└── custom/        ← platform doesn't touch this folder
    └── jira.py    ← your class goes here
achilles-jira-tool/ · separate pip package
# core untouched — discovery finds it via the entry point
├── tool.py         ← the same class following the contract
└── pyproject.toml  [project.entry-points."achilles.tools"]

The contract is neutral: read-only is the policy of the platform presets, while a custom class may write — at the organization's own responsibility, like a custom connector (shared process, no sandbox; the operator decides what to install). The contract < 1.0 isn't frozen — the package is rebuilt against the platform version.

Appearing in the catalog — automatically

The on-screen list is a live type registry, not a table: the tools table stores only state (flags + key) and is linked to the type by name. So a new class enters the catalog on its own — no migration and no UI changes.

Two surfaces — chat and agents → Tool-calling harness

One catalog feeds both surfaces, but their loops differ.

Chat
one round → answer
  • KS search + tools enabled by the admin (chat_enabled)
  • 1..N tools in parallel, e.g. knowledge base + web
Agent
loop ↻ up to the cap
  • a core of 3 KS tools (search · graph · sql, always, locked)
  • + those chosen by the owner from the allowlist

An agent's KS core can't be removed. Removing an added tool is graceful degradation: the agent continues on the core and does not enter the readiness gate (unlike removing the model, which stops the agent).

"Locked" is about configuration, not unconditionality. "Always" and "locked" on the core mean "can't be turned off manually by either admin or owner," not "present in any state of the base." There is a second, derived axis: with an empty base (is_empty) the KS tools aren't supplied at all — there's nothing to search. This is one principle across both surfaces: both search_knowledge in chat and the agent's core fall away while the base holds no data; this is decided by the shared harness, not by an admin mark. Not a "removal" and not config-driven degradation — simply the absence of data; once the base fills, the core returns on its own.

Enablement: two independent marks → AI Tools screen

The catalog is only a type registry; a tool's admission to work is set by two independent marks, one per surface. Both are off by default — a tool enters service only through a deliberate admin action; it stays active as long as at least one mark supplies it.

Chat OFF
chat_enabled — the tool is visible to chat.
set by admin
Agent allowlist OFF
agents_allowed — selectable by the owner in the agent editor.
set by admin

Enabling a tool and changing its settings (provider, key) is an admin action: it lands in the audit log alongside other access changes (the model calls themselves aren't logged there — the log holds significant events, not every request). Default OFF and auditing of enablement form a perimeter of caution: an external exit is opened explicitly and under observation. A rate limit / call budget for web_search is v2.

Health check — a button for the admin → Same as for AI models

The key is set, the provider chosen — but does it work? The admin shouldn't learn of a stale key from user complaints. Every tool with an external exit has a "Test" button: the platform calls the type's probe() (a light provider ping, not a real model request) and stores the result. This mirrors the connection check for AI model providers.

active error unchecked
External servers — MCP v2

Connecting external MCP servers and write tools is the next iteration. The locked design: per-user OAuth (each user authorizes for themselves), the server holds its own ACL, access is read-only via admin curation. In the wireframes — grey inactive stubs.