feat: add agent tools and sanitize tool names for Anthropic API
Add 8 new agent-callable tools (sessions.list/history/create/delete,
agents.list, message.send, cron.list/trigger) and sanitize tool names
at the API boundary (dots → underscores) to comply with Anthropic's
`^[a-zA-Z0-9_-]{1,128}` requirement. Reverse-maps sanitized names
back to internal names for hook callbacks and tool execution.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import type { Tool, ToolResult } from '../types.js';
|
||||
import type { AgentConfigRegistry } from '../../agents/registry.js';
|
||||
|
||||
/**
|
||||
* Creates an agents.list tool bound to the given AgentConfigRegistry.
|
||||
* Lists all registered agent configurations with their settings.
|
||||
*/
|
||||
export function createAgentsListTool(registry: AgentConfigRegistry): Tool {
|
||||
return {
|
||||
name: 'agents.list',
|
||||
description:
|
||||
'List all registered agent configurations. Shows agent names, model tiers, tool profiles, and sandbox status.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
execute: async (_rawArgs: unknown): Promise<ToolResult> => {
|
||||
try {
|
||||
const configs = registry.list();
|
||||
|
||||
if (configs.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
output: 'No agent configurations registered.',
|
||||
};
|
||||
}
|
||||
|
||||
const lines = configs.map((c) => {
|
||||
const parts = [`- **${c.name}**`];
|
||||
if (c.modelTier) parts.push(`tier=${c.modelTier}`);
|
||||
if (c.toolProfile) parts.push(`profile=${c.toolProfile}`);
|
||||
if (c.sandbox) parts.push('sandboxed');
|
||||
if (c.systemPrompt) {
|
||||
const preview = c.systemPrompt.slice(0, 80).replace(/\n/g, ' ');
|
||||
parts.push(`prompt="${preview}${c.systemPrompt.length > 80 ? '...' : ''}"`);
|
||||
}
|
||||
return parts.join(' | ');
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: `${configs.length} agent(s) registered:\n\n${lines.join('\n')}`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { Tool, ToolResult } from '../types.js';
|
||||
import type { CronScheduler } from '../../automation/cron.js';
|
||||
|
||||
/**
|
||||
* Creates cron management tools bound to the given CronScheduler.
|
||||
*/
|
||||
export function createCronTools(scheduler: CronScheduler): Tool[] {
|
||||
const cronList: Tool = {
|
||||
name: 'cron.list',
|
||||
description:
|
||||
'List all configured cron jobs with their names and status.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
execute: async (_rawArgs: unknown): Promise<ToolResult> => {
|
||||
try {
|
||||
const jobNames = scheduler.getJobNames();
|
||||
|
||||
if (jobNames.length === 0) {
|
||||
return { success: true, output: 'No cron jobs configured.' };
|
||||
}
|
||||
|
||||
const lines = jobNames.map((name) => `- ${name}`);
|
||||
return {
|
||||
success: true,
|
||||
output: `${jobNames.length} cron job(s):\n\n${lines.join('\n')}`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const cronTrigger: Tool = {
|
||||
name: 'cron.trigger',
|
||||
description:
|
||||
'Manually trigger a cron job by name, executing it immediately regardless of its schedule.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Name of the cron job to trigger',
|
||||
},
|
||||
},
|
||||
required: ['name'],
|
||||
},
|
||||
execute: async (rawArgs: unknown): Promise<ToolResult> => {
|
||||
const args = rawArgs as { name: string };
|
||||
|
||||
try {
|
||||
const jobNames = scheduler.getJobNames();
|
||||
if (!jobNames.includes(args.name)) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `Cron job "${args.name}" not found. Available: ${jobNames.join(', ')}`,
|
||||
};
|
||||
}
|
||||
|
||||
scheduler.triggerJob(args.name);
|
||||
return {
|
||||
success: true,
|
||||
output: `Cron job "${args.name}" triggered.`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return [cronList, cronTrigger];
|
||||
}
|
||||
@@ -15,6 +15,10 @@ export { createProcessTools, ProcessManager } from './process/index.js';
|
||||
export type { ProcessManagerConfig } from './process/index.js';
|
||||
export { BrowserManager, createBrowserTools } from './browser/index.js';
|
||||
export type { BrowserManagerConfig } from './browser/index.js';
|
||||
export { createSessionTools } from './sessions.js';
|
||||
export { createAgentsListTool } from './agents-list.js';
|
||||
export { createMessageSendTool } from './message-send.js';
|
||||
export { createCronTools } from './cron.js';
|
||||
|
||||
import type { Tool } from '../types.js';
|
||||
import type { MemoryStore } from '../../memory/store.js';
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { Tool, ToolResult } from '../types.js';
|
||||
import type { ChannelRegistry } from '../../channels/registry.js';
|
||||
|
||||
interface MessageSendArgs {
|
||||
channel: string;
|
||||
peerId: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a message.send tool bound to the given ChannelRegistry.
|
||||
* Allows the agent to send messages to any registered channel.
|
||||
*/
|
||||
export function createMessageSendTool(channelRegistry: ChannelRegistry): Tool {
|
||||
return {
|
||||
name: 'message.send',
|
||||
description:
|
||||
'Send a message to a specific user on a specific channel. Use this to proactively reach out to users on Telegram, Discord, Slack, WhatsApp, or other connected channels.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
channel: {
|
||||
type: 'string',
|
||||
description: 'Channel adapter name (e.g. "telegram", "discord", "slack", "whatsapp", "webchat")',
|
||||
},
|
||||
peerId: {
|
||||
type: 'string',
|
||||
description: 'Target user or chat ID on the channel',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
description: 'Message text to send',
|
||||
},
|
||||
},
|
||||
required: ['channel', 'peerId', 'text'],
|
||||
},
|
||||
execute: async (rawArgs: unknown): Promise<ToolResult> => {
|
||||
const args = rawArgs as MessageSendArgs;
|
||||
|
||||
try {
|
||||
const adapter = channelRegistry.get(args.channel);
|
||||
if (!adapter) {
|
||||
const available = channelRegistry.list().map((a) => a.name);
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `Channel "${args.channel}" not found. Available channels: ${available.join(', ')}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (adapter.status !== 'connected') {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `Channel "${args.channel}" is not connected (status: ${adapter.status})`,
|
||||
};
|
||||
}
|
||||
|
||||
await adapter.send(args.peerId, { text: args.text });
|
||||
return {
|
||||
success: true,
|
||||
output: `Message sent to ${args.peerId} on ${args.channel}.`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import type { Tool, ToolResult } from '../types.js';
|
||||
import type { SessionManager } from '../../session/manager.js';
|
||||
|
||||
interface SessionsListArgs {
|
||||
// no args
|
||||
}
|
||||
|
||||
interface SessionsHistoryArgs {
|
||||
sessionId: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
interface SessionsCreateArgs {
|
||||
frontend: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
interface SessionsDeleteArgs {
|
||||
frontend: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates session management tools bound to the given SessionManager.
|
||||
*/
|
||||
export function createSessionTools(sessionManager: SessionManager): Tool[] {
|
||||
const sessionsList: Tool = {
|
||||
name: 'sessions.list',
|
||||
description:
|
||||
'List all active sessions. Returns session IDs and message counts.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
execute: async (_rawArgs: unknown): Promise<ToolResult> => {
|
||||
try {
|
||||
const sessionIds = sessionManager.listSessions();
|
||||
if (sessionIds.length === 0) {
|
||||
return { success: true, output: 'No active sessions.' };
|
||||
}
|
||||
|
||||
const sessions = sessionIds.map((id) => {
|
||||
const parts = id.split(':');
|
||||
const frontend = parts[0];
|
||||
const userId = parts.slice(1).join(':');
|
||||
const session = sessionManager.getSession(frontend, userId);
|
||||
return {
|
||||
id,
|
||||
frontend,
|
||||
userId,
|
||||
messageCount: session.getHistory().length,
|
||||
};
|
||||
});
|
||||
|
||||
const lines = sessions.map(
|
||||
(s) => `- **${s.id}** (${s.messageCount} messages)`,
|
||||
);
|
||||
return { success: true, output: lines.join('\n') };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const sessionsHistory: Tool = {
|
||||
name: 'sessions.history',
|
||||
description:
|
||||
'Get the message history for a specific session. Returns the messages with role and content.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sessionId: {
|
||||
type: 'string',
|
||||
description: 'Session ID in "frontend:userId" format',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of messages to return (default: all)',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
description: 'Number of messages to skip from the start (default: 0)',
|
||||
},
|
||||
},
|
||||
required: ['sessionId'],
|
||||
},
|
||||
execute: async (rawArgs: unknown): Promise<ToolResult> => {
|
||||
const args = rawArgs as SessionsHistoryArgs;
|
||||
|
||||
try {
|
||||
const parts = args.sessionId.split(':');
|
||||
const frontend = parts[0];
|
||||
const userId = parts.slice(1).join(':');
|
||||
const session = sessionManager.getSession(frontend, userId);
|
||||
const allMessages = session.getHistory();
|
||||
|
||||
const start = args.offset ?? 0;
|
||||
const end = args.limit ? start + args.limit : allMessages.length;
|
||||
const messages = allMessages.slice(start, end);
|
||||
|
||||
if (messages.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
output: `Session "${args.sessionId}" has no messages${start > 0 ? ' at this offset' : ''}.`,
|
||||
};
|
||||
}
|
||||
|
||||
const lines = messages.map((m, i) => {
|
||||
const content =
|
||||
typeof m.content === 'string'
|
||||
? m.content.slice(0, 200)
|
||||
: '[multipart]';
|
||||
return `${start + i + 1}. [${m.role}] ${content}`;
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: `Session "${args.sessionId}" (${allMessages.length} total messages):\n\n${lines.join('\n')}`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const sessionsCreate: Tool = {
|
||||
name: 'sessions.create',
|
||||
description:
|
||||
'Create a new session (or get an existing one). Returns the session ID.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
frontend: {
|
||||
type: 'string',
|
||||
description: 'Frontend/channel name (e.g. "telegram", "webchat")',
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
description: 'User ID for this session',
|
||||
},
|
||||
},
|
||||
required: ['frontend', 'userId'],
|
||||
},
|
||||
execute: async (rawArgs: unknown): Promise<ToolResult> => {
|
||||
const args = rawArgs as SessionsCreateArgs;
|
||||
|
||||
try {
|
||||
const session = sessionManager.getSession(args.frontend, args.userId);
|
||||
return {
|
||||
success: true,
|
||||
output: `Session "${session.id}" ready (${session.getHistory().length} existing messages).`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const sessionsDelete: Tool = {
|
||||
name: 'sessions.delete',
|
||||
description:
|
||||
'Clear a session history and close it. The session ID is "frontend:userId".',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
frontend: {
|
||||
type: 'string',
|
||||
description: 'Frontend/channel name',
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
description: 'User ID',
|
||||
},
|
||||
},
|
||||
required: ['frontend', 'userId'],
|
||||
},
|
||||
execute: async (rawArgs: unknown): Promise<ToolResult> => {
|
||||
const args = rawArgs as SessionsDeleteArgs;
|
||||
|
||||
try {
|
||||
const session = sessionManager.getSession(args.frontend, args.userId);
|
||||
const messageCount = session.getHistory().length;
|
||||
session.clear();
|
||||
sessionManager.closeSession(args.frontend, args.userId);
|
||||
return {
|
||||
success: true,
|
||||
output: `Session "${args.frontend}:${args.userId}" cleared (${messageCount} messages removed).`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return [sessionsList, sessionsHistory, sessionsCreate, sessionsDelete];
|
||||
}
|
||||
Reference in New Issue
Block a user