Pattern lift from eyaltoledano/claude-task-master (MIT + Commons Clause — pattern only, no code lift). Adds BOOCODE_TOOLS env var with three tiers: - core (4 tools): view_file, list_dir, grep, find_files. ~2k token schema cost. - standard (15 tools): core + web_search, web_fetch, git_status, all 8 codecontext_* tools. ~10k token schema cost. - all (default; current behavior): every tool in ALL_TOOLS (20). ~21k token schema cost. The env var is a CEILING — narrows agent whitelists, never expands. Default behavior unchanged when var is unset. resolveToolTier is case-insensitive and falls back to 'all' on unknown values. CORE_TOOL_NAMES + STANDARD_TOOL_NAMES validated at module load against TOOLS_BY_NAME via two top-level for-loops that throw on the first missing name. Module fails to import if a tier references a tool that doesn't exist in the registry — catches typos and stale tier definitions at boot rather than silently filtering valid tools out of agent whitelists. Wiring: agents.ts parseAgentBlock now reads BOOCODE_TOOLS from process.env per parse, intersects with the agent's declared frontmatter tools (or DEFAULT_TOOLS when frontmatter omits the field). Per-parse read is fine — agents are re-parsed on the existing 60s cache TTL. Tests: tools.test.ts grows from 1 to 10 tests. Covers resolveToolTier across tiers/case/unknown values + the CORE-subset-of-STANDARD invariant + TOOLS_BY_NAME existence for both tier sets. 204/204 pass (was 195; +9 new). Deviation from the brief: the codecontext tools in the actual registry have NO codecontext_* prefix (the brief's STANDARD list assumed it). Used the actual names (get_codebase_overview, search_symbols, etc.). Module-load validation would have failed boot with the prefixed names. Smoke: with BOOCODE_TOOLS unset, agents return their full 12-tool whitelists. With BOOCODE_TOOLS=core in .env + container restart, the same agents narrow to 4 tools (find_files, grep, list_dir, view_file) — intersection of declared whitelist ∩ core tier. Reverted after confirmation. CLAUDE.md updated with BOOCODE_TOOLS in the Environment section's Optional list. .env.example gained a commented BOOCODE_TOOLS=all line with the per-tier token-cost table. ~110 LoC across 5 files (4 modified + 1 test expansion). Under the brief's ~30 LoC estimate for code; the test suite expansion drove most of the growth.
77 lines
2.8 KiB
TypeScript
77 lines
2.8 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
ALL_TOOLS,
|
|
CORE_TOOL_NAMES,
|
|
STANDARD_TOOL_NAMES,
|
|
TOOLS_BY_NAME,
|
|
resolveToolTier,
|
|
} from '../tools.js';
|
|
|
|
describe('ALL_TOOLS registry', () => {
|
|
// v1.13.3: tools must be alpha-sorted at module load. llama.cpp's prompt
|
|
// cache hits on byte-identical prefixes; the tool list lives near the
|
|
// top of the system prompt, so any order drift invalidates every cached
|
|
// turn. The registry sort is the single source of truth; downstream
|
|
// helpers (toolJsonSchemas, TOOLS_BY_NAME, buildAiTools) inherit it.
|
|
it('exports tools in alphabetical order by name', () => {
|
|
const names = ALL_TOOLS.map((t) => t.name);
|
|
expect(names).toEqual([...names].sort((a, b) => a.localeCompare(b)));
|
|
});
|
|
});
|
|
|
|
describe('resolveToolTier (v1.13.15-tools)', () => {
|
|
it('returns CORE tools for tier=core', () => {
|
|
expect(resolveToolTier('core')).toEqual(CORE_TOOL_NAMES);
|
|
});
|
|
|
|
it('returns STANDARD tools for tier=standard', () => {
|
|
const result = resolveToolTier('standard');
|
|
expect(result.length).toBe(STANDARD_TOOL_NAMES.length);
|
|
expect(result.length).toBeGreaterThan(CORE_TOOL_NAMES.length);
|
|
// STANDARD is a strict superset of CORE.
|
|
expect(result).toEqual(expect.arrayContaining([...CORE_TOOL_NAMES]));
|
|
});
|
|
|
|
it('returns ALL tool names for tier=all', () => {
|
|
expect(resolveToolTier('all').length).toBe(ALL_TOOLS.length);
|
|
});
|
|
|
|
it('defaults to all when env var is undefined', () => {
|
|
expect(resolveToolTier(undefined).length).toBe(ALL_TOOLS.length);
|
|
});
|
|
|
|
it('is case-insensitive', () => {
|
|
expect(resolveToolTier('CORE')).toEqual(CORE_TOOL_NAMES);
|
|
expect(resolveToolTier('Standard').length).toBe(STANDARD_TOOL_NAMES.length);
|
|
});
|
|
|
|
it('falls back to all for unknown tier strings', () => {
|
|
expect(resolveToolTier('bogus').length).toBe(ALL_TOOLS.length);
|
|
});
|
|
});
|
|
|
|
describe('CORE_TOOL_NAMES + STANDARD_TOOL_NAMES validation', () => {
|
|
// The module-load validation in tools.ts throws if a tier references a
|
|
// tool that doesn't exist in TOOLS_BY_NAME. These tests double-check that
|
|
// invariant from the consumer side so a future tier-list edit can't smuggle
|
|
// in a typo without a test failure.
|
|
it('every CORE name exists in TOOLS_BY_NAME', () => {
|
|
for (const name of CORE_TOOL_NAMES) {
|
|
expect(TOOLS_BY_NAME[name], `CORE references unknown tool '${name}'`).toBeDefined();
|
|
}
|
|
});
|
|
|
|
it('every STANDARD name exists in TOOLS_BY_NAME', () => {
|
|
for (const name of STANDARD_TOOL_NAMES) {
|
|
expect(TOOLS_BY_NAME[name], `STANDARD references unknown tool '${name}'`).toBeDefined();
|
|
}
|
|
});
|
|
|
|
it('CORE is a subset of STANDARD', () => {
|
|
const standardSet = new Set<string>(STANDARD_TOOL_NAMES);
|
|
for (const name of CORE_TOOL_NAMES) {
|
|
expect(standardSet.has(name), `'${name}' is in CORE but not STANDARD`).toBe(true);
|
|
}
|
|
});
|
|
});
|