blob: d79c719d0252b992c22bbd0a9470db87bc996488 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import assert from 'node:assert/strict';
import { describe, test } from 'node:test';
import { z } from 'zod';
import {
TOOL_SEARCH_MAX_SCHEMA_CHARS,
TOOL_SEARCH_NAME,
ToolAvailabilityRuntime,
toolAvailabilityHash,
type ToolSearchResult,
} from '../tool-availability.js';
import { bindToolActivationIdentity, toolActivationKey } from '../tool-activation-identity.js';
import type { MakaTool, MakaToolContext } from '../tool-runtime.js';
function tool(name: string, description = name): MakaTool {
return { name, description, parameters: z.object({}), impl: () => ({ ok: true }) };
}
function withCategory(name: string, categoryHint: MakaTool['categoryHint']): MakaTool {
return {
name,
description: name,
parameters: z.object({}),
impl: () => ({ ok: true }),
categoryHint,
};
}
const invalid: MakaTool = {
name: 'invalid',
description: 'invalid',
parameters: z.object({}),
impl: () => ({}),
};
const ctx: MakaToolContext = {
sessionId: 's',
turnId: 't',
cwd: '/tmp',
toolCallId: 'tc',
abortSignal: new AbortController().signal,
emitOutput: () => {},
};
test('tool availability hash canonicalizes group members', () => {
const grouped = toolAvailabilityHash({
groups: [{ id: 'docs', toolNames: ['docs_read', 'docs_edit', 'docs_read'] }],
});
const reordered = toolAvailabilityHash({
groups: [{ id: 'docs', toolNames: ['docs_edit', 'docs_read'] }],
});
assert.equal(grouped, reordered);
});
test('tool availability hash distinguishes full and search-enabled bindings', () => {
assert.notEqual(toolAvailabilityHash(undefined), toolAvailabilityHash({}));
});
function runtime() {
return new ToolAvailabilityRuntime(
[
tool('Read'),
tool('Write'),
tool('browser_click', 'Click an element in the browser'),
tool('docs_edit', 'Edit a document'),
tool('docs_read', 'Read a document'),
],
{
groups: [
{ id: 'browser', toolNames: ['browser_click'], description: 'Browser automation' },
{ id: 'docs', toolNames: ['docs_edit', 'docs_read'], description: 'Document tools' },
],
},
invalid,
);
}
function searchTool(plan: ReturnType<ToolAvailabilityRuntime['prepare']>): MakaTool {
const connector = plan.providerTools.find((candidate) => candidate.name === TOOL_SEARCH_NAME);
assert.ok(connector);
return connector;
}
describe('ToolAvailabilityRuntime — search activation', () => {
test('step 0 exposes direct tools and tool_search, but not searchable schemas', () => {
const plan = runtime().prepare(new Map());
assert.ok(plan.activeTools.includes('Read'));
assert.ok(plan.activeTools.includes('Write'));
assert.ok(plan.activeTools.includes(TOOL_SEARCH_NAME));
assert.ok(!plan.activeTools.includes('browser_click'));
assert.ok(!plan.activeTools.includes('docs_edit'));
});
test('a group cannot defer the fixed direct baseline', () => {
const plan = new ToolAvailabilityRuntime(
[tool('Read'), tool('browser_click')],
{ groups: [{ id: 'bad-source', toolNames: ['Read', 'browser_click'] }] },
invalid,
).prepare(new Map());
assert.ok(plan.activeTools.includes('Read'));
assert.ok(!plan.activeTools.includes('browser_click'));
assert.doesNotMatch(searchTool(plan).description, /- Read/);
});
test('skill discovery tools stay direct while search is enabled', () => {
const plan = new ToolAvailabilityRuntime(
[tool('Skill'), tool('SkillSearch'), tool('custom')],
{},
invalid,
).prepare(new Map());
assert.deepEqual(plan.activeTools, ['Skill', 'SkillSearch', TOOL_SEARCH_NAME]);
});
test('provider-routed apply_patch inherits direct editing visibility', () => {
const plan = new ToolAvailabilityRuntime(
[tool('apply_patch'), tool('custom')],
{},
invalid,
).prepare(new Map());
assert.deepEqual(plan.activeTools, ['apply_patch', TOOL_SEARCH_NAME]);
});
test('inventory contains group and canonical names without tool descriptions', () => {
const connector = searchTool(runtime().prepare(new Map()));
assert.match(connector.description, /browser:\n- browser_click/);
assert.match(connector.description, /docs:\n- docs_edit\n- docs_read/);
assert.doesNotMatch(connector.description, /Click an element/);
assert.doesNotMatch(connector.description, /Edit a document/);
});
test('search indexes group meaning without exposing it in the initial inventory', async () => {
const plan = new ToolAvailabilityRuntime(
[tool('remote_invoke', 'Invoke a provider-defined operation')],
{
groups: [
{
id: 'calendar_provider',
label: 'Team calendar',
description: 'Schedule a calendar meeting and manage events.',
toolNames: ['remote_invoke'],
},
],
},
invalid,
).prepare(new Map());
const connector = searchTool(plan);
assert.doesNotMatch(connector.description, /Team calendar|Schedule a calendar meeting/);
assert.deepEqual(await connector.impl({ query: 'schedule calendar meeting' }, ctx), {
activated: ['remote_invoke'],
});
});
test('a successful search activates bounded matches for the next projection', async () => {
const active = new Map<string, string>();
const traces: Record<string, unknown>[] = [];
const plan = runtime().prepare(active);
const connector = searchTool(plan);
const tracedContext: MakaToolContext = {
...ctx,
emitRunTrace: (type, _message, data) => {
if (type === 'tool_searched') traces.push(data ?? {});
},
};
assert.deepEqual(await connector.impl({ query: 'edit document', limit: 1 }, tracedContext), {
activated: ['docs_edit'],
});
assert.ok(active.has('docs_edit'), 'turn-owned activation map changed');
assert.ok(
!plan.currentRepairToolNames().includes('docs_edit'),
'step snapshot stayed immutable',
);
const next = plan.projectActiveTools!();
assert.ok(next.activeTools.includes('docs_edit'));
assert.ok(!next.activeTools.includes('docs_read'));
assert.equal(traces[0]?.query, 'edit document');
assert.deepEqual(traces[0]?.activated, ['docs_edit']);
});
test('a same-name replacement does not inherit the retired contribution activation', async () => {
const first = bindToolActivationIdentity(tool('plugin_weather', 'weather'), {
kind: 'plugin',
scopeId: 'profile',
entryId: 'weather-entry',
extensionId: 'weather-plugin',
generation: 1,
toolName: 'plugin_weather',
});
const active = new Map<string, string>();
const initial = new ToolAvailabilityRuntime(
[first],
{ groups: [{ id: 'plugins', toolNames: ['plugin_weather'] }] },
invalid,
).prepare(active);
await searchTool(initial).impl({ query: 'weather' }, ctx);
assert.equal(active.get('plugin_weather'), toolActivationKey(first));
const replacement = bindToolActivationIdentity(tool('plugin_weather', 'weather'), {
kind: 'plugin',
scopeId: 'profile',
entryId: 'weather-entry',
extensionId: 'weather-plugin',
generation: 2,
toolName: 'plugin_weather',
});
const next = new ToolAvailabilityRuntime(
[replacement],
{ groups: [{ id: 'plugins', toolNames: ['plugin_weather'] }] },
invalid,
).prepare(active);
assert.equal(active.has('plugin_weather'), false);
assert.equal(next.activeTools.includes('plugin_weather'), false);
});
test('an equivalent rebuilt Host wrapper keeps its activation', async () => {
const active = new Map<string, string>();
const first = tool('todo_read', 'read the session todo');
const initial = new ToolAvailabilityRuntime(
[first],
{ groups: [{ id: 'todo', toolNames: ['todo_read'] }] },
invalid,
).prepare(active);
await searchTool(initial).impl({ query: 'read todo' }, ctx);
const rebuilt = tool('todo_read', 'read the session todo');
assert.notEqual(rebuilt, first);
const next = new ToolAvailabilityRuntime(
[rebuilt],
{ groups: [{ id: 'todo', toolNames: ['todo_read'] }] },
invalid,
).prepare(active);
assert.equal(active.get('todo_read'), toolActivationKey(rebuilt));
assert.equal(next.activeTools.includes('todo_read'), true);
});
test('ordinary result is thin and contains no complete schemas', async () => {
const connector = searchTool(runtime().prepare(new Map()));
const output = await connector.impl({ query: 'browser click' }, ctx);
assert.deepEqual(output, { activated: ['browser_click'] });
assert.deepEqual(await connector.toModelOutput?.({ toolCallId: 'tc', input: {}, output }), {
type: 'json',
value: { activated: ['browser_click'] },
});
});
test('repeated and parallel searches union and deduplicate turn activation', async () => {
const active = new Map<string, string>();
const plan = runtime().prepare(active);
const connector = searchTool(plan);
await Promise.all([
connector.impl({ query: 'document read', limit: 1 }, ctx),
connector.impl({ query: 'browser click', limit: 1 }, ctx),
connector.impl({ query: 'browser click', limit: 1 }, ctx),
]);
assert.deepEqual([...active.keys()].sort(), ['browser_click', 'docs_read']);
});
test('already-active matches do not consume a later search limit or schema budget', async () => {
const active = new Map<string, string>();
const largeDescription = `Perform a calendar action ${'x'.repeat(40 * 1024)}`;
const plan = new ToolAvailabilityRuntime(
[tool('calendar_primary', largeDescription), tool('calendar_secondary', largeDescription)],
{
groups: [
{
id: 'calendar',
toolNames: ['calendar_primary', 'calendar_secondary'],
},
],
},
invalid,
).prepare(active);
const connector = searchTool(plan);
const first = (await connector.impl(
{ query: 'calendar action', limit: 1 },
ctx,
)) as ToolSearchResult;
const second = (await connector.impl(
{ query: 'calendar action', limit: 2 },
ctx,
)) as ToolSearchResult;
assert.equal(first.activated.length, 1);
assert.equal(second.activated.length, 1);
assert.notEqual(second.activated[0], first.activated[0]);
assert.equal(active.size, 2);
});
test('reports and skips an oversized tool without hiding a smaller later match', async () => {
const active = new Map<string, string>();
const plan = new ToolAvailabilityRuntime(
[
tool('oversized_target', `Oversized target ${'x'.repeat(TOOL_SEARCH_MAX_SCHEMA_CHARS)}`),
tool('smaller_fallback', 'An oversized target fallback'),
],
{
groups: [
{
id: 'oversized',
toolNames: ['oversized_target', 'smaller_fallback'],
},
],
},
invalid,
).prepare(active);
const connector = searchTool(plan);
const result = (await connector.impl({ query: 'oversized target' }, ctx)) as ToolSearchResult;
assert.deepEqual(result.activated, ['smaller_fallback']);
assert.equal(result.blocked?.name, 'oversized_target');
assert.equal(result.blocked?.reason, 'schema_too_large');
assert.ok((result.blocked?.schemaChars ?? 0) > TOOL_SEARCH_MAX_SCHEMA_CHARS);
assert.equal(active.has('smaller_fallback'), true);
assert.deepEqual(
await connector.toModelOutput?.({ toolCallId: 'tc', input: {}, output: result }),
{
type: 'json',
value: { activated: ['smaller_fallback'], blocked: result.blocked },
},
);
});
test('stops at the schema ceiling instead of silently changing relevance order', async () => {
const largeDescription = `Budget branch ${'x'.repeat(40 * 1024)}`;
const active = new Map<string, string>();
const plan = new ToolAvailabilityRuntime(
[
tool('budget_branch_primary', largeDescription),
tool('budget_branch_secondary', largeDescription),
tool('lower_ranked_tool', 'A lower ranked budget branch tool'),
],
{
groups: [
{
id: 'budget',
toolNames: ['budget_branch_primary', 'budget_branch_secondary', 'lower_ranked_tool'],
},
],
},
invalid,
).prepare(active);
const result = (await searchTool(plan).impl(
{ query: 'budget branch', limit: 3 },
ctx,
)) as ToolSearchResult;
assert.equal(result.activated.length, 1);
assert.equal(result.blocked?.reason, 'schema_budget_exhausted');
assert.equal(active.size, 1);
assert.equal(active.has('lower_ranked_tool'), false);
});
test('required orchestration tools are visible without changing activation state', () => {
const active = new Map<string, string>();
const plan = runtime().prepare(active, new Set(['docs_read']));
assert.ok(plan.activeTools.includes('docs_read'));
assert.equal(active.size, 0);
assert.ok(plan.projectActiveTools!().activeTools.includes('docs_read'));
});
test('activation maps isolate overlapping and subsequent turns', async () => {
const first = new Map<string, string>();
const firstPlan = runtime().prepare(first);
await searchTool(firstPlan).impl({ query: 'browser click' }, ctx);
assert.ok(firstPlan.projectActiveTools!().activeTools.includes('browser_click'));
const secondPlan = runtime().prepare(new Map());
assert.ok(!secondPlan.activeTools.includes('browser_click'));
});
test('an ungrouped bound tool is deferred by default', () => {
const plan = new ToolAvailabilityRuntime(
[tool('Read'), tool('future_tool')],
{},
invalid,
).prepare(new Map());
assert.deepEqual(plan.activeTools, ['Read', TOOL_SEARCH_NAME]);
assert.match(searchTool(plan).description, /- future_tool/);
assert.ok(plan.gating?.gatedNames.has('future_tool'));
});
test('omitting availability keeps an explicit binding fully visible', () => {
const plan = new ToolAvailabilityRuntime(
[tool('Read'), tool('custom')],
undefined,
invalid,
).prepare(new Map());
assert.deepEqual(plan.activeTools, ['custom', 'Read']);
assert.ok(!plan.providerTools.some((candidate) => candidate.name === TOOL_SEARCH_NAME));
assert.equal(plan.gating, undefined);
});
test('buckets ungrouped native tools into capability families by categoryHint', () => {
const plan = new ToolAvailabilityRuntime(
[
withCategory('agent_spawn', 'subagent'),
withCategory('web_search', 'web_read'),
withCategory('screen_click', 'computer_use'),
withCategory('session_tool', 'custom_tool'), // no family mapping -> other
tool('legacy_tool'), // no categoryHint -> other
],
{ groups: [] },
invalid,
).prepare(new Map());
const bySource = plan.diagnostics([], 0)!.visibleToolNamesBySource!;
// Distinct permission hints land in distinct browsing families, not one `other`.
assert.deepEqual(bySource.agents, ['agent_spawn']);
assert.deepEqual(bySource.web, ['web_search']);
assert.deepEqual(bySource.computer_use, ['screen_click']);
// Only hint-less / custom_tool tools fall back to `other`.
assert.deepEqual(bySource.other, ['legacy_tool', 'session_tool']);
// A hint present in the exhaustive family map but mapped to `null`
// (custom_tool) still resolves to `other`, not a family of its own.
assert.equal(bySource.custom_tool, undefined);
// Family ids surface in the searchable inventory the model sees.
const description = searchTool(plan).description;
assert.match(description, /agents:\n- agent_spawn/);
assert.match(description, /web:\n- web_search/);
assert.match(description, /computer_use:\n- screen_click/);
});
test('a caller-supplied group keeps precedence over a categoryHint family', () => {
const plan = new ToolAvailabilityRuntime(
[withCategory('agent_spawn', 'subagent'), withCategory('agent_list', 'subagent')],
{ groups: [{ id: 'orchestration', label: 'Orchestration', toolNames: ['agent_spawn'] }] },
invalid,
).prepare(new Map());
const bySource = plan.diagnostics([], 0)!.visibleToolNamesBySource!;
// The explicit group claims agent_spawn; only the remaining hinted tool is family-bucketed.
assert.deepEqual(bySource.orchestration, ['agent_spawn']);
assert.deepEqual(bySource.agents, ['agent_list']);
});
});