feat: add customTools support to AgentConfig for orchestrator-level tool injection
Users can now pass custom ToolDefinition objects via AgentConfig.customTools, which are registered alongside built-in tools in all orchestrator paths (runAgent, runTeam, runTasks). Custom tools bypass allowlist/preset filtering but can still be blocked by disallowedTools. Ref #108
This commit is contained in:
parent
017e0f42f6
commit
38a88df144
|
|
@ -212,6 +212,11 @@ function resolveTokenBudget(primary?: number, fallback?: number): number | undef
|
||||||
function buildAgent(config: AgentConfig): Agent {
|
function buildAgent(config: AgentConfig): Agent {
|
||||||
const registry = new ToolRegistry()
|
const registry = new ToolRegistry()
|
||||||
registerBuiltInTools(registry)
|
registerBuiltInTools(registry)
|
||||||
|
if (config.customTools) {
|
||||||
|
for (const tool of config.customTools) {
|
||||||
|
registry.register(tool, { runtimeAdded: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
const executor = new ToolExecutor(registry)
|
const executor = new ToolExecutor(registry)
|
||||||
return new Agent(config, registry, executor)
|
return new Agent(config, registry, executor)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
src/types.ts
10
src/types.ts
|
|
@ -223,6 +223,16 @@ export interface AgentConfig {
|
||||||
/** API key override; falls back to the provider's standard env var. */
|
/** API key override; falls back to the provider's standard env var. */
|
||||||
readonly apiKey?: string
|
readonly apiKey?: string
|
||||||
readonly systemPrompt?: string
|
readonly systemPrompt?: string
|
||||||
|
/**
|
||||||
|
* Custom tool definitions to register alongside built-in tools.
|
||||||
|
* Created via `defineTool()`. Custom tools bypass `tools` (allowlist)
|
||||||
|
* and `toolPreset` filtering, but can still be blocked by `disallowedTools`.
|
||||||
|
*
|
||||||
|
* Tool names must not collide with built-in tool names; a duplicate name
|
||||||
|
* will throw at registration time.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
readonly customTools?: readonly ToolDefinition<any>[]
|
||||||
/** Names of tools (from the tool registry) available to this agent. */
|
/** Names of tools (from the tool registry) available to this agent. */
|
||||||
readonly tools?: readonly string[]
|
readonly tools?: readonly string[]
|
||||||
/** Names of tools explicitly disallowed for this agent. */
|
/** Names of tools explicitly disallowed for this agent. */
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,56 @@ describe('OpenMultiAgent', () => {
|
||||||
expect(oma.getStatus().completedTasks).toBe(1)
|
expect(oma.getStatus().completedTasks).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('registers customTools so they are available to the LLM', async () => {
|
||||||
|
mockAdapterResponses = ['used custom tool']
|
||||||
|
|
||||||
|
const { z } = await import('zod')
|
||||||
|
const { defineTool } = await import('../src/tool/framework.js')
|
||||||
|
|
||||||
|
const myTool = defineTool({
|
||||||
|
name: 'my_custom_tool',
|
||||||
|
description: 'A custom tool for testing',
|
||||||
|
inputSchema: z.object({ query: z.string() }),
|
||||||
|
execute: async ({ query }) => ({ data: query }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const oma = new OpenMultiAgent({ defaultModel: 'mock-model' })
|
||||||
|
await oma.runAgent(
|
||||||
|
{ ...agentConfig('solo'), customTools: [myTool] },
|
||||||
|
'Use the custom tool',
|
||||||
|
)
|
||||||
|
|
||||||
|
const toolNames = capturedChatOptions[0]?.tools?.map(t => t.name) ?? []
|
||||||
|
expect(toolNames).toContain('my_custom_tool')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('customTools bypass tools allowlist and toolPreset filtering', async () => {
|
||||||
|
mockAdapterResponses = ['done']
|
||||||
|
|
||||||
|
const { z } = await import('zod')
|
||||||
|
const { defineTool } = await import('../src/tool/framework.js')
|
||||||
|
|
||||||
|
const myTool = defineTool({
|
||||||
|
name: 'my_custom_tool',
|
||||||
|
description: 'A custom tool for testing',
|
||||||
|
inputSchema: z.object({ query: z.string() }),
|
||||||
|
execute: async ({ query }) => ({ data: query }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const oma = new OpenMultiAgent({ defaultModel: 'mock-model' })
|
||||||
|
|
||||||
|
// toolPreset 'readonly' only allows file_read, grep, glob — custom tool should still appear
|
||||||
|
await oma.runAgent(
|
||||||
|
{ ...agentConfig('solo'), customTools: [myTool], toolPreset: 'readonly' },
|
||||||
|
'test',
|
||||||
|
)
|
||||||
|
|
||||||
|
const toolNames = capturedChatOptions[0]?.tools?.map(t => t.name) ?? []
|
||||||
|
expect(toolNames).toContain('my_custom_tool')
|
||||||
|
// built-in tools outside the preset should be filtered
|
||||||
|
expect(toolNames).not.toContain('bash')
|
||||||
|
})
|
||||||
|
|
||||||
it('fires onProgress events', async () => {
|
it('fires onProgress events', async () => {
|
||||||
mockAdapterResponses = ['done']
|
mockAdapterResponses = ['done']
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue