Skip to content

Tool System Design

Tools let the agent fetch information, work with chat files, run code, and update application state. A tool has to be installed, admitted by server configuration, and active for the chat before the agent can use it.

For available tools and their parameters, see Tools Reference. For operator setup, see Secure Tool Access.

Tool Architecture Overview

installed Python modules
    -> registry: startup policy and availability checks
    -> agent: core tools plus selected custom tools
    -> protocol adapter: function schemas sent to the model
    -> executor: active-tool check and execution
    -> agent: result messages sent back to the model

The registry and executor run in the application process. Installed tool code is trusted server code; the code interpreter's sandbox does not sandbox every Python tool module.

Tool Discovery and Registration

ToolRegistry scans top-level Python modules in src/numi_chat/tools/ at startup. It imports eligible modules, discovers tool classes, and registers instances that pass its filters. Restart the application after adding modules or changing startup tool configuration.

The BaseTool Abstraction

BaseTool provides the shared contract:

Member Purpose
name, description, parameters Function name and JSON schema shown to the model
execute() Tool implementation
is_available(), availability_reason() Dependency/configuration availability checks
concurrency_safe Whether calls may share a parallel batch; defaults to true
_ok(), _error(), _noop() Helpers returning JSON text with a status field

Most tools check availability during registration. A tool can defer that check with check_availability_on_register=False. The registry logs an exception from an availability check and can still register the tool, so registration is not proof that an external service is healthy.

Safety Controls

Server policy and chat selection are separate gates:

  1. Startup policy: TOOLS_DENYLIST wins over TOOLS_ALLOWLIST. An ordinary tool must be allowlisted to register. request_tool_activation is exempt from the allowlist, but can still be denylisted.
  2. Chat selection: Registered core tools are active by default. Registered custom tools are sent only when enabled for that chat.
  3. Execution: The executor rejects a model call whose name is outside the chat's active set, even if the model invents or remembers that name.

request_tool_activation lets the agent ask the user to enable an available custom group. The request itself does not enable it. The agent omits this tool when no registered custom group remains disabled.

The executor parses JSON arguments and injects server-owned chat_id, chat_uuid, and user_id where applicable. These values override any model arguments with the same names. Tool implementations remain responsible for validating their inputs and enforcing file, account, and network boundaries; a function schema alone is not runtime validation.

Code Interpreter Sandboxing

Sandboxed code_interpreter execution first tries Monty for eligible scripts without third-party dependencies. Unsupported scripts or Monty failures can fall back to mcp-run-python with Deno/Pyodide, which is also used for third-party libraries such as plotting packages.

The tool validates code and dependency requests, provisions validated current-chat files, and bounds execution, output, and generated artifacts. Variables do not persist between calls. Saved files can be reused through the current chat's file paths.

Networking is disabled by default. In the Deno path, package loading can use network access during setup; when networking is disabled, that permission is revoked before user code runs. TOOLS_CODE_INTERPRETER_UNSANDBOXED=true explicitly switches to host Python with the server account's filesystem, process, and network access.

Timeouts and application checks do not provide a general resource-isolation boundary for all tools. See Security Model for deployment controls and Tools Reference for interpreter limits.

Parallel Execution Strategy

ToolExecutorService uses a shared ThreadPoolExecutor with bounded admission. Within a model step, it partitions calls by concurrency_safe:

  • Concurrency-safe calls run in parallel batches.
  • Calls marked unsafe, including code_interpreter, run one at a time after the parallel batches.

This ordering applies within that execution batch; it is not a global promise that calls from different chats cannot overlap. A tool needing shared-state protection must provide it in its implementation.

The browser receives results as calls finish. The agent then saves tool messages in the original call order so IDs and results remain paired for the next model request.

Error Isolation

Invalid JSON, inactive tools, execution failures, and exhausted worker capacity produce tool error results. A failed call does not discard successful sibling results. Oversized output is replaced by an error with a bounded preview.

Cancellation prevents pending calls from starting where possible. Already running calls are awaited before reporting cancellation, because their side effects cannot be safely stopped by cancelling a thread.

Tool Schema Generation

BaseTool.to_schema() produces a function schema with the tool's name, description, and parameters. The agent supplies schemas only for its active set; protocol adapters convert them as needed for Chat Completions, Responses, or Anthropic Messages.

Schema Compaction

With TOOLS_SCHEMA_MODE=compact, the registry can truncate function descriptions and remove parameter descriptions and titles according to the schema settings. This reduces prompt text but also removes guidance the model may need. It does not change which tools are active or their execution checks.

Tool System Reminders (Output-Only Steering)

A tool can declare system_hints with the @tool_system_hints decorator in src/numi_chat/tools/base.py. The agent selects a hint by the result's action or mode, falling back to *, and adds it to the tool message as system_hint when no hint is already present.

These hints steer the next model step. They are not part of the function schema and do not grant permissions or enforce policy.

Adding a New Tool

Follow Adding Custom Tools for a complete implementation. Adding a class makes it discoverable; startup policy and chat activation still determine whether a model can call it.

The source files to follow are:

Source under src/numi_chat/ Responsibility
tools/base.py Tool contract, schemas, output helpers, hints
tools/registry.py Discovery, registration, core/custom classification
services/tool_executor.py Active-tool checks, context injection, concurrency, output limits
agent/runtime.py Active schema selection, tool events, result persistence, hints
tools/code_interpreter.py Code validation, sandbox selection, file provisioning