kimi-code / packages /agent-core-v2 /src /agent /toolSelect /toolSelectService.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
68d7816 verified
Raw
History Blame Contribute Delete
11.7 kB
import { Service } from '#/_base/di/service';
import { LifecycleScope } from '#/app/scopes';
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
import { defineState } from '#/state/state';
import { IEventBus } from '#/app/event/eventBus';
import { IFlagService } from '#/app/flag/flag';
import type { ToolDescription as Tool } from '#human/llm/message';
import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory';
import { ContextSpliced } from '#/agent/contextMemory/contextEvents';
import type { ContextMessage } from '#/agent/contextMemory/types';
import { CompactionCompleted } from '#/agent/fullCompaction/compactionOps';
import { IAgentProfileService } from '#/agent/profile/profile';
import { IAgentStateService } from '#/agent/state/agentState';
import { IAgentToolPolicyService } from '#/agent/toolPolicy/toolPolicy';
import { isMcpToolName, type ToolInfo } from '#/tool/toolContract';
import { IAgentToolExecutorService } from '#/agent/toolExecutor/toolExecutor';
import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry';
import {
collectLoadedDynamicToolNames,
foldAnnouncedToolNames,
renderLoadableToolsAnnouncement,
stripDynamicToolContext,
} from './dynamicTools';
import { TOOL_SELECT_FLAG_ID } from './flag';
import {
IAgentToolSelectService,
SELECT_TOOLS_TOOL_NAME,
type LoadToolsResult,
type ShapedToolEntry,
} from './toolSelect';
export const toolSelectPendingLoadedKey = defineState<Set<string>>(
'toolSelect.pendingLoaded',
() => new Set(),
);
export class AgentToolSelectService extends Service implements IAgentToolSelectService {
declare readonly _serviceBrand: undefined;
constructor(
@IAgentToolRegistryService private readonly toolRegistry: IAgentToolRegistryService,
@IAgentProfileService private readonly profile: IAgentProfileService,
@IAgentToolPolicyService private readonly toolPolicy: IAgentToolPolicyService,
@IAgentContextMemoryService private readonly context: IAgentContextMemoryService,
@IAgentToolExecutorService toolExecutor: IAgentToolExecutorService,
@IFlagService private readonly flags: IFlagService,
@IEventBus eventBus: IEventBus,
@IAgentStateService private readonly states: IAgentStateService,
) {
super();
this.states.contributeState(toolSelectPendingLoadedKey);
this._register(
toolExecutor.registerUnavailableToolDescriber((name) => this.describeUnavailableTool(name)),
);
this._register(
toolExecutor.registerMissingToolDescriber((name) => this.describeMissingTool(name)),
);
this._register(
eventBus.subscribe(CompactionCompleted, () => {
this.pendingLoaded.clear();
}),
);
this._register(
eventBus.subscribe(ContextSpliced, (splice) => {
if (splice.deleteCount === 0 || splice.messages.length > 0) return;
this.dropPendingLoadedNotLanded();
}),
);
}
private get pendingLoaded(): Set<string> {
return this.states.get(toolSelectPendingLoadedKey);
}
private dropPendingLoadedNotLanded(): void {
if (this.pendingLoaded.size === 0) return;
const landed = collectLoadedDynamicToolNames(this.context.get());
for (const name of this.pendingLoaded) {
if (!landed.has(name)) this.pendingLoaded.delete(name);
}
}
enabled(): boolean {
const capabilities = this.profile.getModelCapabilities();
return (
capabilities.dynamically_loaded_tools === true &&
capabilities.tool_use &&
this.flags.enabled(TOOL_SELECT_FLAG_ID)
);
}
shapeTools(entries: readonly ToolInfo[]): readonly ShapedToolEntry[] {
const disclosure = this.enabled();
const activeEntries = this.activeEntries(entries, disclosure);
if (!disclosure) return activeEntries;
const loaded = this.loadedToolNames();
const shaped: ShapedToolEntry[] = [];
for (const entry of activeEntries) {
if (entry.name === SELECT_TOOLS_TOOL_NAME) {
shaped.push(entry);
continue;
}
if (!this.isDynamicallyLoadable(entry)) {
shaped.push(entry);
continue;
}
if (!loaded.has(entry.name)) continue;
shaped.push({ ...entry, deferred: true });
}
return shaped;
}
shapeHistory(messages: readonly ContextMessage[]): readonly ContextMessage[] {
if (this.enabled()) return this.shapeActiveHistory(messages);
return stripDynamicToolContext(messages);
}
load(names: readonly string[]): LoadToolsResult {
const loadable = new Set(this.loadableToolNames());
const loaded = this.activeLoadedToolNames();
const toLoad: string[] = [];
const alreadyAvailable: string[] = [];
const unknown: string[] = [];
for (const name of new Set(names)) {
if (loaded.has(name)) {
alreadyAvailable.push(name);
} else if (loadable.has(name)) {
toLoad.push(name);
} else {
unknown.push(name);
}
}
if (toLoad.length > 0) {
for (const name of toLoad) this.pendingLoaded.add(name);
}
return { toLoad, alreadyAvailable, unknown };
}
drainPendingToolSchemas(): readonly Tool[] | undefined {
if (!this.enabled() || this.pendingLoaded.size === 0) return undefined;
const names = [...this.pendingLoaded].toSorted((a, b) => a.localeCompare(b));
const tools: Tool[] = [];
for (const name of names) {
const tool = this.schemaOf(name);
if (tool === undefined) continue;
this.pendingLoaded.delete(name);
tools.push(tool);
}
return tools.length === 0 ? undefined : tools;
}
loadableToolsAnnouncement(): string | undefined {
if (!this.enabled()) return undefined;
const loadable = this.loadableToolNames();
const loadableSet = new Set(loadable);
const announced = foldAnnouncedToolNames(this.context.get());
const added = loadable.filter((name) => !announced.has(name));
const removed = [...announced]
.filter((name) => !loadableSet.has(name))
.toSorted((a, b) => a.localeCompare(b));
if (added.length === 0 && removed.length === 0) return undefined;
return renderLoadableToolsAnnouncement(added, removed);
}
private shouldIntercept(name: string): boolean {
if (!this.enabled()) return false;
const info = this.toolRegistry.list().find((entry) => entry.name === name);
if (info === undefined || !this.isDynamicallyLoadable(info)) return false;
if (!this.loadableToolNames().includes(name)) return false;
return !this.activeLoadedToolNames().has(name);
}
private describeUnavailableTool(name: string): string | undefined {
if (this.isInactiveLoadedTool(name)) return inactiveLoadedToolOutput(name);
if (!this.shouldIntercept(name)) return undefined;
return notLoadedToolOutput(name);
}
private describeMissingTool(name: string): string | undefined {
if (!this.enabled()) return undefined;
if (this.toolRegistry.resolve(name) !== undefined) return undefined;
if (!this.loadedToolNames().has(name)) return undefined;
if (isMcpToolName(name)) {
return (
`Tool "${name}" was loaded but its MCP server is currently disconnected. ` +
'It may become available again when the server reconnects; do not retry immediately.'
);
}
return (
`Tool "${name}" was loaded but is no longer registered. ` +
'Do not retry it unless it becomes available again.'
);
}
private loadableToolNames(): string[] {
return this.toolRegistry
.list()
.filter(
(info) =>
this.isDynamicallyLoadable(info) &&
this.toolPolicy.isToolActive(info.name, info.source),
)
.map((info) => info.name)
.toSorted((a, b) => a.localeCompare(b));
}
private loadedToolNames(): Set<string> {
const names = collectLoadedDynamicToolNames(this.context.get());
for (const name of this.pendingLoaded) names.add(name);
return names;
}
private activeLoadedToolNames(): Set<string> {
const names = this.loadedToolNames();
for (const name of names) {
if (!this.isLoadedToolActive(name)) names.delete(name);
}
return names;
}
private isInactiveLoadedTool(name: string): boolean {
if (!this.enabled()) return false;
return this.loadedToolNames().has(name) && !this.isLoadedToolActive(name);
}
private isLoadedToolActive(name: string): boolean {
const info = this.toolRegistry.list().find((entry) => entry.name === name);
if (info !== undefined) {
return (
this.isDynamicallyLoadable(info) &&
this.toolPolicy.isToolActive(name, info.source)
);
}
if (isMcpToolName(name)) return this.toolPolicy.isToolActive(name, 'mcp');
return false;
}
private isDynamicallyLoadable(info: ToolInfo): boolean {
return info.disclosure === 'deferred';
}
private shapeActiveHistory(messages: readonly ContextMessage[]): readonly ContextMessage[] {
let shaped: ContextMessage[] | undefined;
for (let i = 0; i < messages.length; i += 1) {
const message = messages[i]!;
const next = this.shapeActiveMessage(message);
if (next === message) {
if (shaped !== undefined) shaped.push(message);
continue;
}
if (shaped === undefined) shaped = messages.slice(0, i);
if (next !== undefined) shaped.push(next);
}
return shaped ?? messages;
}
private shapeActiveMessage(message: ContextMessage): ContextMessage | undefined {
const tools = message.tools;
if (tools === undefined || tools.length === 0) return message;
let kept: Tool[] | undefined;
for (let i = 0; i < tools.length; i += 1) {
const tool = tools[i]!;
if (this.isLoadedToolActive(tool.name)) {
if (kept !== undefined) kept.push(tool);
continue;
}
if (kept === undefined) kept = tools.slice(0, i);
}
if (kept === undefined) return message;
if (kept.length > 0) return { ...message, tools: kept };
const { tools: _tools, ...rest } = message;
void _tools;
if (rest.content.length === 0 && rest.toolCalls.length === 0) return undefined;
return rest;
}
private schemaOf(name: string): Tool | undefined {
const tool = this.toolRegistry.resolve(name);
if (tool === undefined) return undefined;
return {
name: tool.name,
description: tool.description,
parameters: tool.parameters,
};
}
private activeEntries(entries: readonly ToolInfo[], disclosure: boolean): readonly ToolInfo[] {
let filtered: ToolInfo[] | undefined;
for (let i = 0; i < entries.length; i += 1) {
const entry = entries[i]!;
const active =
this.toolPolicy.isToolActive(entry.name, entry.source) ||
(disclosure &&
entry.name === SELECT_TOOLS_TOOL_NAME &&
this.toolPolicy.isToolActiveForDisclosure(entry.name, entry.source));
const keep = active && (disclosure || entry.name !== SELECT_TOOLS_TOOL_NAME);
if (keep) {
if (filtered !== undefined) filtered.push(entry);
continue;
}
if (filtered === undefined) filtered = entries.slice(0, i);
}
return filtered ?? entries;
}
}
function notLoadedToolOutput(name: string): string {
return (
`Tool "${name}" is available but not loaded. ` +
`Call select_tools with ["${name}"] first, then call the tool.`
);
}
function inactiveLoadedToolOutput(name: string): string {
return (
`Tool "${name}" was loaded but is no longer active. ` +
'Ask the user to enable it before calling it again.'
);
}
registerScopedService(
LifecycleScope.Agent,
IAgentToolSelectService,
AgentToolSelectService,
ScopeActivation.OnScopeCreated,
'toolSelect',
);