From 78551d68f32c6835837315be1c35d1af863bac19 Mon Sep 17 00:00:00 2001 From: kurihada Date: Wed, 17 Dec 2025 14:15:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20ToolParameter=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20default=E3=80=81minimum=E3=80=81maximum=E3=80=81enu?= =?UTF-8?q?m=20=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 扩展 ToolParameter 类型,新增 default/minimum/maximum/enum 可选字段 - 更新 buildZodSchema 函数处理 enum 和 min/max 约束 - MCP 工具适配器解析 JSON Schema 中的扩展字段 - task_output 工具使用新字段替代 description 中的约束说明 - 新增单元测试覆盖所有扩展字段 --- packages/core/src/mcp/tool-adapter.ts | 22 ++- packages/core/src/tools/task/task_output.ts | 137 +++++++++++++++++++ packages/core/src/types/index.ts | 31 ++++- packages/core/tests/unit/types/index.test.ts | 115 ++++++++++++++++ 4 files changed, 301 insertions(+), 4 deletions(-) create mode 100644 packages/core/src/tools/task/task_output.ts diff --git a/packages/core/src/mcp/tool-adapter.ts b/packages/core/src/mcp/tool-adapter.ts index 92c0fc5..ff5e48d 100644 --- a/packages/core/src/mcp/tool-adapter.ts +++ b/packages/core/src/mcp/tool-adapter.ts @@ -51,17 +51,37 @@ function convertInputSchema( const properties = schema.properties as Record | undefined; const required = (schema.required as string[]) || []; if (properties) { for (const [name, prop] of Object.entries(properties)) { - parameters[name] = { + const param: ToolParameter = { type: convertJsonSchemaType(prop.type), description: prop.description || '', required: required.includes(name), }; + + // 可选字段 + if (prop.default !== undefined) { + param.default = prop.default; + } + if (prop.minimum !== undefined) { + param.minimum = prop.minimum; + } + if (prop.maximum !== undefined) { + param.maximum = prop.maximum; + } + if (prop.enum && Array.isArray(prop.enum)) { + param.enum = prop.enum; + } + + parameters[name] = param; } } diff --git a/packages/core/src/tools/task/task_output.ts b/packages/core/src/tools/task/task_output.ts new file mode 100644 index 0000000..673522f --- /dev/null +++ b/packages/core/src/tools/task/task_output.ts @@ -0,0 +1,137 @@ +import type { ToolWithMetadata } from '../types.js'; +import { getAgentManager } from '../../agent/manager.js'; + +/** + * TaskOutput 工具 + * 用于获取后台 Agent 的执行结果 + */ +export const taskOutputTool: ToolWithMetadata = { + name: 'task_output', + description: `获取后台运行的 Agent 执行结果。 + +当使用 task 工具的 run_in_background 参数启动后台 Agent 后, +使用此工具查询执行状态和结果。 + +使用示例: +- 查询结果(阻塞等待): task_output({ task_id: "abc123" }) +- 检查状态(不阻塞): task_output({ task_id: "abc123", block: false }) +- 设置超时: task_output({ task_id: "abc123", timeout: 60000 })`, + parameters: { + task_id: { + type: 'string', + description: 'The task ID to get output from', + required: true, + }, + block: { + type: 'boolean', + description: 'Whether to wait for completion', + required: false, + default: true, + }, + timeout: { + type: 'number', + description: 'Max wait time in ms', + required: false, + default: 30000, + minimum: 0, + maximum: 600000, + }, + }, + metadata: { + name: 'task_output', + category: 'agent', + description: '获取后台 Agent 执行结果', + keywords: ['task', 'output', 'result', 'background', '结果', '后台', '查询'], + deferLoading: false, // 核心工具,始终加载 + }, + async execute(params) { + const { + task_id, + block = true, + timeout = 30000, + } = params as { + task_id: string; + block?: boolean; + timeout?: number; + }; + + const agentManager = getAgentManager(); + + // 验证超时参数(毫秒),转换为秒传给 agentManager + const timeoutMs = Math.min(Math.max(timeout, 0), 600000); + const timeoutSec = Math.ceil(timeoutMs / 1000); + + // 获取 Agent 输出 + const agent = await agentManager.getAgentOutput(task_id, block, timeoutSec); + + if (!agent) { + return { + success: false, + output: '', + error: `Agent ${task_id} 不存在。请检查 task_id 是否正确。`, + }; + } + + // 根据状态返回不同结果 + if (agent.status === 'running') { + const runningTime = Math.round((Date.now() - agent.startedAt.getTime()) / 1000); + return { + success: true, + output: `Agent ${task_id} 仍在运行中...\n` + + `- 类型: ${agent.agentName}\n` + + `- 任务: ${agent.description}\n` + + `- 已运行: ${runningTime} 秒\n\n` + + `稍后再次调用 task_output 查询结果,或使用 block: true 等待完成。`, + metadata: { + agentId: agent.id, + status: 'running', + agentName: agent.agentName, + runningTime, + }, + }; + } + + if (agent.status === 'failed') { + const duration = agent.completedAt + ? Math.round((agent.completedAt.getTime() - agent.startedAt.getTime()) / 1000) + : 0; + return { + success: false, + output: '', + error: `Agent ${task_id} 执行失败:\n` + + `- 类型: ${agent.agentName}\n` + + `- 任务: ${agent.description}\n` + + `- 耗时: ${duration} 秒\n` + + `- 错误: ${agent.error || '未知错误'}`, + metadata: { + agentId: agent.id, + status: 'failed', + agentName: agent.agentName, + duration, + }, + }; + } + + // completed + const duration = agent.completedAt + ? Math.round((agent.completedAt.getTime() - agent.startedAt.getTime()) / 1000) + : 0; + + return { + success: true, + output: `## Agent ${task_id} 执行完成\n\n` + + `- 类型: ${agent.agentName}\n` + + `- 任务: ${agent.description}\n` + + `- 耗时: ${duration} 秒\n` + + `- 步数: ${agent.steps || 0}\n\n` + + `### 结果\n\n${agent.result || '(无输出)'}`, + metadata: { + agentId: agent.id, + status: 'completed', + agentName: agent.agentName, + duration, + steps: agent.steps, + }, + }; + }, +}; diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index d55e9ec..4c44976 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -44,6 +44,14 @@ export interface ToolParameter { type: 'string' | 'number' | 'boolean' | 'array' | 'object'; description: string; required?: boolean; + /** 默认值 */ + default?: unknown; + /** 最小值(仅用于 number 类型) */ + minimum?: number; + /** 最大值(仅用于 number 类型) */ + maximum?: number; + /** 枚举值(仅用于 string 类型) */ + enum?: string[]; } // 工具执行结果 @@ -105,11 +113,24 @@ export function buildZodSchema(parameters: Record): z.Zod switch (param.type) { case 'string': - fieldSchema = z.string().describe(param.description); + if (param.enum && param.enum.length > 0) { + // 使用 enum 约束 + fieldSchema = z.enum(param.enum as [string, ...string[]]).describe(param.description); + } else { + fieldSchema = z.string().describe(param.description); + } break; - case 'number': - fieldSchema = z.number().describe(param.description); + case 'number': { + let numSchema = z.number(); + if (param.minimum !== undefined) { + numSchema = numSchema.min(param.minimum); + } + if (param.maximum !== undefined) { + numSchema = numSchema.max(param.maximum); + } + fieldSchema = numSchema.describe(param.description); break; + } case 'boolean': fieldSchema = z.boolean().describe(param.description); break; @@ -127,6 +148,10 @@ export function buildZodSchema(parameters: Record): z.Zod fieldSchema = fieldSchema.optional(); } + // 注意:default 值在 JSON Schema 中传递给 LLM,但 Zod 的 .default() + // 是运行时行为,这里不应用 .default(),保持 optional 即可 + // LLM 会根据 JSON Schema 中的 default 值决定是否传递参数 + schemaObj[key] = fieldSchema; } diff --git a/packages/core/tests/unit/types/index.test.ts b/packages/core/tests/unit/types/index.test.ts index 5caf7a4..d946629 100644 --- a/packages/core/tests/unit/types/index.test.ts +++ b/packages/core/tests/unit/types/index.test.ts @@ -246,6 +246,121 @@ describe('buildZodSchema', () => { }); }); + describe('扩展字段', () => { + it('enum 约束字符串类型', () => { + const parameters: Record = { + color: { + type: 'string', + description: '颜色选择', + required: true, + enum: ['red', 'green', 'blue'], + }, + }; + + const schema = buildZodSchema(parameters); + + expect(schema.safeParse({ color: 'red' }).success).toBe(true); + expect(schema.safeParse({ color: 'green' }).success).toBe(true); + expect(schema.safeParse({ color: 'yellow' }).success).toBe(false); + }); + + it('minimum 约束数字类型', () => { + const parameters: Record = { + count: { + type: 'number', + description: '数量', + required: true, + minimum: 1, + }, + }; + + const schema = buildZodSchema(parameters); + + expect(schema.safeParse({ count: 1 }).success).toBe(true); + expect(schema.safeParse({ count: 100 }).success).toBe(true); + expect(schema.safeParse({ count: 0 }).success).toBe(false); + expect(schema.safeParse({ count: -1 }).success).toBe(false); + }); + + it('maximum 约束数字类型', () => { + const parameters: Record = { + limit: { + type: 'number', + description: '限制', + required: true, + maximum: 100, + }, + }; + + const schema = buildZodSchema(parameters); + + expect(schema.safeParse({ limit: 100 }).success).toBe(true); + expect(schema.safeParse({ limit: 50 }).success).toBe(true); + expect(schema.safeParse({ limit: 101 }).success).toBe(false); + }); + + it('minimum 和 maximum 组合约束', () => { + const parameters: Record = { + timeout: { + type: 'number', + description: '超时时间(毫秒)', + required: false, + minimum: 0, + maximum: 600000, + }, + }; + + const schema = buildZodSchema(parameters); + + expect(schema.safeParse({}).success).toBe(true); + expect(schema.safeParse({ timeout: 0 }).success).toBe(true); + expect(schema.safeParse({ timeout: 30000 }).success).toBe(true); + expect(schema.safeParse({ timeout: 600000 }).success).toBe(true); + expect(schema.safeParse({ timeout: -1 }).success).toBe(false); + expect(schema.safeParse({ timeout: 600001 }).success).toBe(false); + }); + + it('default 字段不影响 Zod 验证', () => { + // default 只用于 JSON Schema,Zod 不会自动应用默认值 + const parameters: Record = { + block: { + type: 'boolean', + description: '是否阻塞', + required: false, + default: true, + }, + }; + + const schema = buildZodSchema(parameters); + + // 不提供值时仍然是 undefined(不是 true) + const result = schema.safeParse({}); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.block).toBeUndefined(); + } + + // 显式提供值时使用提供的值 + expect(schema.safeParse({ block: false }).success).toBe(true); + }); + + it('空 enum 数组使用普通 string', () => { + const parameters: Record = { + text: { + type: 'string', + description: '文本', + required: true, + enum: [], + }, + }; + + const schema = buildZodSchema(parameters); + + // 空 enum 应该退化为普通 string + expect(schema.safeParse({ text: 'anything' }).success).toBe(true); + }); + }); + describe('实际工具参数示例', () => { it('bash 工具参数', () => { const bashParameters: Record = {