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:
- Startup policy:
TOOLS_DENYLISTwins overTOOLS_ALLOWLIST. An ordinary tool must be allowlisted to register.request_tool_activationis exempt from the allowlist, but can still be denylisted. - Chat selection: Registered core tools are active by default. Registered custom tools are sent only when enabled for that chat.
- 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 |