6090508bad
- Add curly braces to all if/else/for/while statements - Fix indentation and trailing spaces - Auto-fixed 372 linting errors using eslint --fix - Remaining issues are warnings only (non-null assertions, explicit any types)
128 lines
3.5 KiB
TypeScript
128 lines
3.5 KiB
TypeScript
import type { Tool, ToolInputSchema } from './types.js';
|
|
import type { ToolPolicy, ToolPolicyContext } from './policy.js';
|
|
|
|
export interface AnthropicToolDef {
|
|
name: string;
|
|
description: string;
|
|
input_schema: ToolInputSchema;
|
|
}
|
|
|
|
export interface OpenAIToolDef {
|
|
type: 'function';
|
|
function: {
|
|
name: string;
|
|
description: string;
|
|
parameters: ToolInputSchema;
|
|
};
|
|
}
|
|
|
|
export class ToolRegistry {
|
|
private tools: Map<string, Tool> = new Map();
|
|
private _policy?: ToolPolicy;
|
|
|
|
/** Sanitize a tool name for API compatibility (dots → underscores). */
|
|
static sanitizeToolName(name: string): string {
|
|
return name.replace(/\./g, '_');
|
|
}
|
|
|
|
register(tool: Tool): void {
|
|
if (this.tools.has(tool.name)) {
|
|
throw new Error(`Tool '${tool.name}' is already registered`);
|
|
}
|
|
this.tools.set(tool.name, tool);
|
|
}
|
|
|
|
unregister(name: string): boolean {
|
|
return this.tools.delete(name);
|
|
}
|
|
|
|
/** Replace an existing tool with a new implementation. Throws if not registered. */
|
|
replace(tool: Tool): void {
|
|
if (!this.tools.has(tool.name)) {
|
|
throw new Error(`Tool '${tool.name}' is not registered — cannot replace`);
|
|
}
|
|
this.tools.set(tool.name, tool);
|
|
}
|
|
|
|
/** Create a shallow clone of this registry (new Map, same Tool objects + policy). */
|
|
clone(): ToolRegistry {
|
|
const cloned = new ToolRegistry();
|
|
for (const tool of this.tools.values()) {
|
|
cloned.register(tool);
|
|
}
|
|
if (this._policy) {
|
|
cloned.setPolicy(this._policy);
|
|
}
|
|
return cloned;
|
|
}
|
|
|
|
get(name: string): Tool | undefined {
|
|
return this.tools.get(name);
|
|
}
|
|
|
|
/** Resolve a tool by its API-sanitized name (underscores → dots fallback). */
|
|
getByApiName(name: string): Tool | undefined {
|
|
return this.tools.get(name) ?? this.tools.get(name.replace(/_/g, '.'));
|
|
}
|
|
|
|
list(): Tool[] {
|
|
return Array.from(this.tools.values());
|
|
}
|
|
|
|
/** Set the tool policy for filtering. */
|
|
setPolicy(policy: ToolPolicy): void {
|
|
this._policy = policy;
|
|
}
|
|
|
|
/** Get the tool policy (if set). */
|
|
getPolicy(): ToolPolicy | undefined {
|
|
return this._policy;
|
|
}
|
|
|
|
/** Return tools filtered by the policy for a given context. */
|
|
filteredList(context?: ToolPolicyContext): Tool[] {
|
|
if (!this._policy) {return this.list();}
|
|
return this._policy.filterTools(this.list(), context);
|
|
}
|
|
|
|
toAnthropicFormat(): AnthropicToolDef[] {
|
|
return this.list().map(t => ({
|
|
name: ToolRegistry.sanitizeToolName(t.name),
|
|
description: t.description,
|
|
input_schema: t.inputSchema,
|
|
}));
|
|
}
|
|
|
|
/** Return Anthropic-format tools filtered by policy. */
|
|
filteredToAnthropicFormat(context?: ToolPolicyContext): AnthropicToolDef[] {
|
|
return this.filteredList(context).map(t => ({
|
|
name: ToolRegistry.sanitizeToolName(t.name),
|
|
description: t.description,
|
|
input_schema: t.inputSchema,
|
|
}));
|
|
}
|
|
|
|
toOpenAIFormat(): OpenAIToolDef[] {
|
|
return this.list().map(t => ({
|
|
type: 'function' as const,
|
|
function: {
|
|
name: ToolRegistry.sanitizeToolName(t.name),
|
|
description: t.description,
|
|
parameters: t.inputSchema,
|
|
},
|
|
}));
|
|
}
|
|
|
|
/** Return OpenAI-format tools filtered by policy. */
|
|
filteredToOpenAIFormat(context?: ToolPolicyContext): OpenAIToolDef[] {
|
|
return this.filteredList(context).map(t => ({
|
|
type: 'function' as const,
|
|
function: {
|
|
name: ToolRegistry.sanitizeToolName(t.name),
|
|
description: t.description,
|
|
parameters: t.inputSchema,
|
|
},
|
|
}));
|
|
}
|
|
}
|