Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-tools-activate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@moonshot-ai/kimi-code': patch
---

Fix v2 headless runs so progressive MCP tool disclosure exposes `select_tools`.
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
* contributions (`toolRegistry`, L3) into entries of the per-agent runtime
* registry: a contribution activates only when its `when` predicate holds
* and its declared `name` is allowed by the bound Profile's tool policy
* (`profile`, L4). `AgentLifecycleService.create` awaits one activation pass
* after restore and profile binding, so an Agent's tools reflect the Profile
* before the first turn. Bound at Agent scope.
* (`profile`, L4), except for the progressive-disclosure `select_tools`
* gateway while that feature is enabled. `AgentLifecycleService.create`
* awaits one activation pass after restore and profile binding, so an Agent's
* tools reflect the Profile before the first turn. Bound at Agent scope.
*/

import { createDecorator } from '#/_base/di/instantiation';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
* by the bound Profile's tool policy (`profile`), resolves the Agent-scope
* service through the container — nothing constructs the tool before this
* `accessor.get` — and registers the real instance into the runtime
* registry.
* registry. When progressive tool disclosure is enabled, the `select_tools`
* gateway is also activated unless an explicit policy disables it.
*
* Activation runs once explicitly from `AgentLifecycleService.create` (after
* restore and profile binding) and re-runs on every `agent.status.updated`
Expand All @@ -28,8 +29,10 @@ import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/
import { IEventBus } from '#/app/event/eventBus';
import { IAgentProfileService } from '#/agent/profile/profile';
import { isToolActive } from '#/agent/toolPolicy/evaluate';
import { IAgentToolPolicyService } from '#/agent/toolPolicy/toolPolicy';
import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry';
import { getAgentToolContributions } from '#/agent/toolRegistry/toolContribution';
import { IAgentToolSelectService, SELECT_TOOLS_TOOL_NAME } from '#/agent/toolSelect/toolSelect';

import { IAgentToolActivationService } from './toolActivation';

Expand Down Expand Up @@ -57,7 +60,15 @@ export class AgentToolActivationService extends Disposable implements IAgentTool
for (const { id, options } of getAgentToolContributions()) {
const source = options.source ?? 'builtin';
if (this.toolRegistry.resolve(options.name) !== undefined) continue;
if (!isToolActive(policy, options.name, source)) continue;
const active =
isToolActive(policy, options.name, source) ||
(options.name === SELECT_TOOLS_TOOL_NAME &&
accessor.get(IAgentToolSelectService).enabled() &&
accessor.get(IAgentToolPolicyService).isToolActiveForDisclosure(
options.name,
source,
));
if (!active) continue;
if (options.when !== undefined && !options.when(accessor)) continue;
const tool = accessor.get(id);
this._register(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { IEventBus } from '#/app/event/eventBus';
import { IAgentProfileService, type ProfileData } from '#/agent/profile/profile';
import { IAgentToolActivationService } from '#/agent/toolActivation/toolActivation';
import { AgentToolActivationService } from '#/agent/toolActivation/toolActivationService';
import { IAgentToolPolicyService } from '#/agent/toolPolicy/toolPolicy';
import {
_clearAgentToolContributionsForTests,
getAgentToolContributions,
Expand All @@ -20,6 +21,7 @@ import {
} from '#/agent/toolRegistry/toolContribution';
import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry';
import { AgentToolRegistryService } from '#/agent/toolRegistry/toolRegistryService';
import { IAgentToolSelectService, SELECT_TOOLS_TOOL_NAME } from '#/agent/toolSelect/toolSelect';
import type { AgentTool, ToolExecution } from '#/tool/toolContract';

class StubTool implements AgentTool {
Expand All @@ -35,10 +37,12 @@ class StubTool implements AgentTool {
const IAlphaTool = createDecorator<AgentTool>('activationTestAlphaTool');
const IBetaTool = createDecorator<AgentTool>('activationTestBetaTool');
const IGammaTool = createDecorator<AgentTool>('activationTestGammaTool');
const ISelectToolsTool = createDecorator<AgentTool>('activationTestSelectToolsTool');

let alphaConstructions = 0;
let betaConstructions = 0;
let gammaConstructions = 0;
let selectToolsConstructions = 0;

class AlphaTool extends StubTool {
constructor() {
Expand All @@ -61,9 +65,18 @@ class GammaTool extends StubTool {
}
}

class SelectToolsTool extends StubTool {
constructor() {
super(SELECT_TOOLS_TOOL_NAME);
selectToolsConstructions += 1;
}
}

describe('AgentToolActivationService', () => {
let savedContributions: readonly AgentToolContribution[];
let disposables: DisposableStore;
let toolSelectEnabled: boolean;
let disclosureToolActive: boolean;
const profileData: {
activeToolNames?: readonly string[];
disallowedTools?: readonly string[];
Expand All @@ -80,11 +93,18 @@ describe('AgentToolActivationService', () => {
reg.definePartialInstance(IEventBus, {
subscribe: () => toDisposable(() => {}),
});
reg.definePartialInstance(IAgentToolSelectService, {
enabled: () => toolSelectEnabled,
});
reg.definePartialInstance(IAgentToolPolicyService, {
isToolActiveForDisclosure: () => disclosureToolActive,
});
reg.define(IAgentToolRegistryService, AgentToolRegistryService);
reg.define(IAgentToolActivationService, AgentToolActivationService);
reg.define(IAlphaTool, AlphaTool);
reg.define(IBetaTool, BetaTool);
reg.define(IGammaTool, GammaTool);
reg.define(ISelectToolsTool, SelectToolsTool);
},
});
}
Expand All @@ -95,6 +115,9 @@ describe('AgentToolActivationService', () => {
alphaConstructions = 0;
betaConstructions = 0;
gammaConstructions = 0;
selectToolsConstructions = 0;
toolSelectEnabled = false;
disclosureToolActive = true;
_clearAgentToolContributionsForTests();
delete profileData.activeToolNames;
delete profileData.disallowedTools;
Expand Down Expand Up @@ -135,6 +158,50 @@ describe('AgentToolActivationService', () => {
expect(registry.resolve('Beta')).toBeInstanceOf(BetaTool);
});

it('activates select_tools for disclosure when the profile omits it', async () => {
profileData.activeToolNames = ['Alpha', 'mcp__*'];
toolSelectEnabled = true;
registerAgentToolService(ISelectToolsTool, SelectToolsTool, {
name: SELECT_TOOLS_TOOL_NAME,
});
const ix = createActivationHost();

await ix.get(IAgentToolActivationService).activate();

expect(ix.get(IAgentToolRegistryService).resolve(SELECT_TOOLS_TOOL_NAME)).toBeInstanceOf(
SelectToolsTool,
);
expect(selectToolsConstructions).toBe(1);
});

it('does not activate select_tools through disclosure when the gate is closed', async () => {
profileData.activeToolNames = ['Alpha', 'mcp__*'];
registerAgentToolService(ISelectToolsTool, SelectToolsTool, {
name: SELECT_TOOLS_TOOL_NAME,
});
const ix = createActivationHost();

await ix.get(IAgentToolActivationService).activate();

expect(ix.get(IAgentToolRegistryService).resolve(SELECT_TOOLS_TOOL_NAME)).toBeUndefined();
expect(selectToolsConstructions).toBe(0);
});

it('does not activate select_tools through disclosure when the policy disables it', async () => {
profileData.activeToolNames = ['Alpha', 'mcp__*'];
toolSelectEnabled = true;
disclosureToolActive = false;
registerAgentToolService(ISelectToolsTool, SelectToolsTool, {
name: SELECT_TOOLS_TOOL_NAME,
});
const ix = createActivationHost();

await ix.get(IAgentToolActivationService).activate();

expect(ix.get(IAgentToolRegistryService).resolve(SELECT_TOOLS_TOOL_NAME)).toBeUndefined();
expect(selectToolsConstructions).toBe(0);
});

it('activates only the tools allowed by the profile allowlist', async () => {
profileData.activeToolNames = ['Alpha'];
registerAgentToolService(IAlphaTool, AlphaTool, { name: 'Alpha' });
Expand Down