feat(core): ToolParameter 支持 default、minimum、maximum、enum 字段

- 扩展 ToolParameter 类型,新增 default/minimum/maximum/enum 可选字段
- 更新 buildZodSchema 函数处理 enum 和 min/max 约束
- MCP 工具适配器解析 JSON Schema 中的扩展字段
- task_output 工具使用新字段替代 description 中的约束说明
- 新增单元测试覆盖所有扩展字段
This commit is contained in:
2025-12-17 14:15:09 +08:00
parent fe6ef9be9b
commit 78551d68f3
4 changed files with 301 additions and 4 deletions
+21 -1
View File
@@ -51,17 +51,37 @@ function convertInputSchema(
const properties = schema.properties as Record<string, {
type?: string | string[];
description?: string;
default?: unknown;
minimum?: number;
maximum?: number;
enum?: string[];
}> | 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;
}
}
+137
View File
@@ -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,
},
};
},
};
+28 -3
View File
@@ -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<string, ToolParameter>): 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<string, ToolParameter>): z.Zod
fieldSchema = fieldSchema.optional();
}
// 注意:default 值在 JSON Schema 中传递给 LLM,但 Zod 的 .default()
// 是运行时行为,这里不应用 .default(),保持 optional 即可
// LLM 会根据 JSON Schema 中的 default 值决定是否传递参数
schemaObj[key] = fieldSchema;
}
@@ -246,6 +246,121 @@ describe('buildZodSchema', () => {
});
});
describe('扩展字段', () => {
it('enum 约束字符串类型', () => {
const parameters: Record<string, ToolParameter> = {
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<string, ToolParameter> = {
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<string, ToolParameter> = {
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<string, ToolParameter> = {
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 SchemaZod 不会自动应用默认值
const parameters: Record<string, ToolParameter> = {
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<string, ToolParameter> = {
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<string, ToolParameter> = {