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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -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 Schema,Zod 不会自动应用默认值
|
||||
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> = {
|
||||
|
||||
Reference in New Issue
Block a user