openclaw/src/agents/tools-effective-inventory.ts

232 lines
7.3 KiB
TypeScript

import type { OpenClawConfig } from "../config/config.js";
import { getPluginToolMeta } from "../plugins/tools.js";
import { resolveAgentDir, resolveAgentWorkspaceDir, resolveSessionAgentId } from "./agent-scope.js";
import { getChannelAgentToolMeta } from "./channel-tools.js";
import { resolveModel } from "./pi-embedded-runner/model.js";
import { createOpenClawCodingTools } from "./pi-tools.js";
import { resolveEffectiveToolPolicy } from "./pi-tools.policy.js";
import { summarizeToolDescriptionText } from "./tool-description-summary.js";
import { resolveToolDisplay } from "./tool-display.js";
import type { AnyAgentTool } from "./tools/common.js";
export type EffectiveToolSource = "core" | "plugin" | "channel";
export type EffectiveToolInventoryEntry = {
id: string;
label: string;
description: string;
rawDescription: string;
source: EffectiveToolSource;
pluginId?: string;
channelId?: string;
};
export type EffectiveToolInventoryGroup = {
id: EffectiveToolSource;
label: string;
source: EffectiveToolSource;
tools: EffectiveToolInventoryEntry[];
};
export type EffectiveToolInventoryResult = {
agentId: string;
profile: string;
groups: EffectiveToolInventoryGroup[];
};
export type ResolveEffectiveToolInventoryParams = {
cfg: OpenClawConfig;
agentId?: string;
sessionKey?: string;
workspaceDir?: string;
agentDir?: string;
messageProvider?: string;
senderIsOwner?: boolean;
senderId?: string | null;
senderName?: string | null;
senderUsername?: string | null;
senderE164?: string | null;
accountId?: string | null;
modelProvider?: string;
modelId?: string;
currentChannelId?: string;
currentThreadTs?: string;
currentMessageId?: string | number;
groupId?: string | null;
groupChannel?: string | null;
groupSpace?: string | null;
replyToMode?: "off" | "first" | "all";
modelHasVision?: boolean;
requireExplicitMessageTarget?: boolean;
disableMessageTool?: boolean;
};
function resolveEffectiveToolLabel(tool: AnyAgentTool): string {
const rawLabel = typeof tool.label === "string" ? tool.label.trim() : "";
if (rawLabel && rawLabel.toLowerCase() !== tool.name.toLowerCase()) {
return rawLabel;
}
return resolveToolDisplay({ name: tool.name }).title;
}
function resolveRawToolDescription(tool: AnyAgentTool): string {
return typeof tool.description === "string" ? tool.description.trim() : "";
}
function summarizeToolDescription(tool: AnyAgentTool): string {
return summarizeToolDescriptionText({
rawDescription: resolveRawToolDescription(tool),
displaySummary: tool.displaySummary,
});
}
function resolveEffectiveToolSource(tool: AnyAgentTool): {
source: EffectiveToolSource;
pluginId?: string;
channelId?: string;
} {
const pluginMeta = getPluginToolMeta(tool);
if (pluginMeta) {
return { source: "plugin", pluginId: pluginMeta.pluginId };
}
const channelMeta = getChannelAgentToolMeta(tool as never);
if (channelMeta) {
return { source: "channel", channelId: channelMeta.channelId };
}
return { source: "core" };
}
function groupLabel(source: EffectiveToolSource): string {
switch (source) {
case "plugin":
return "Connected tools";
case "channel":
return "Channel tools";
default:
return "Built-in tools";
}
}
function disambiguateLabels(entries: EffectiveToolInventoryEntry[]): EffectiveToolInventoryEntry[] {
const counts = new Map<string, number>();
for (const entry of entries) {
counts.set(entry.label, (counts.get(entry.label) ?? 0) + 1);
}
return entries.map((entry) => {
if ((counts.get(entry.label) ?? 0) < 2) {
return entry;
}
const suffix = entry.pluginId ?? entry.channelId ?? entry.id;
return { ...entry, label: `${entry.label} (${suffix})` };
});
}
function resolveEffectiveModelCompat(params: {
cfg: OpenClawConfig;
agentDir: string;
modelProvider?: string;
modelId?: string;
}) {
const provider = params.modelProvider?.trim();
const modelId = params.modelId?.trim();
if (!provider || !modelId) {
return undefined;
}
try {
return resolveModel(provider, modelId, params.agentDir, params.cfg).model?.compat;
} catch {
return undefined;
}
}
export function resolveEffectiveToolInventory(
params: ResolveEffectiveToolInventoryParams,
): EffectiveToolInventoryResult {
const agentId =
params.agentId?.trim() ||
resolveSessionAgentId({ sessionKey: params.sessionKey, config: params.cfg });
const workspaceDir = params.workspaceDir ?? resolveAgentWorkspaceDir(params.cfg, agentId);
const agentDir = params.agentDir ?? resolveAgentDir(params.cfg, agentId);
const modelCompat = resolveEffectiveModelCompat({
cfg: params.cfg,
agentDir,
modelProvider: params.modelProvider,
modelId: params.modelId,
});
const effectiveTools = createOpenClawCodingTools({
agentId,
sessionKey: params.sessionKey,
workspaceDir,
agentDir,
config: params.cfg,
modelProvider: params.modelProvider,
modelId: params.modelId,
modelCompat,
messageProvider: params.messageProvider,
senderIsOwner: params.senderIsOwner,
senderId: params.senderId,
senderName: params.senderName ?? undefined,
senderUsername: params.senderUsername ?? undefined,
senderE164: params.senderE164 ?? undefined,
agentAccountId: params.accountId ?? undefined,
currentChannelId: params.currentChannelId,
currentThreadTs: params.currentThreadTs,
currentMessageId: params.currentMessageId,
groupId: params.groupId ?? undefined,
groupChannel: params.groupChannel ?? undefined,
groupSpace: params.groupSpace ?? undefined,
replyToMode: params.replyToMode,
allowGatewaySubagentBinding: true,
modelHasVision: params.modelHasVision,
requireExplicitMessageTarget: params.requireExplicitMessageTarget,
disableMessageTool: params.disableMessageTool,
});
const effectivePolicy = resolveEffectiveToolPolicy({
config: params.cfg,
agentId,
sessionKey: params.sessionKey,
modelProvider: params.modelProvider,
modelId: params.modelId,
});
const profile = effectivePolicy.providerProfile ?? effectivePolicy.profile ?? "full";
const entries = disambiguateLabels(
effectiveTools
.map((tool) => {
const source = resolveEffectiveToolSource(tool);
return {
id: tool.name,
label: resolveEffectiveToolLabel(tool),
description: summarizeToolDescription(tool),
rawDescription: resolveRawToolDescription(tool) || summarizeToolDescription(tool),
...source,
} satisfies EffectiveToolInventoryEntry;
})
.toSorted((a, b) => a.label.localeCompare(b.label)),
);
const groupsBySource = new Map<EffectiveToolSource, EffectiveToolInventoryEntry[]>();
for (const entry of entries) {
const tools = groupsBySource.get(entry.source) ?? [];
tools.push(entry);
groupsBySource.set(entry.source, tools);
}
const groups = (["core", "plugin", "channel"] as const)
.map((source) => {
const tools = groupsBySource.get(source);
if (!tools || tools.length === 0) {
return null;
}
return {
id: source,
label: groupLabel(source),
source,
tools,
} satisfies EffectiveToolInventoryGroup;
})
.filter((group): group is EffectiveToolInventoryGroup => group !== null);
return { agentId, profile, groups };
}