diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite index 31bd9e6..9fdb0e1 100644 Binary files a/packages/drizzle-pkg/db.sqlite and b/packages/drizzle-pkg/db.sqlite differ diff --git a/packages/drizzle-pkg/seed.ts b/packages/drizzle-pkg/seed.ts index 03e2355..01e22d0 100644 --- a/packages/drizzle-pkg/seed.ts +++ b/packages/drizzle-pkg/seed.ts @@ -240,6 +240,23 @@ const TOOLS_SEED: ToolSeed[] = [ adminOnly: 0, sortOrder: 150, }, + { + id: "tool_agent_invoke", + name: "agent-invoke", + slug: "agent-invoke", + description: "调用其他 Agent 处理子任务,实现多 Agent 协作", + type: "agent-invoke", + config: { + maxRecursionDepth: 3, + resultTruncateLength: 8000, + timeoutMs: 60000, + maxOutputTokens: 8192, + }, + enabled: 1, + needsApproval: 0, + adminOnly: 0, + sortOrder: 160, + }, ]; async function seedTools() { @@ -266,7 +283,7 @@ async function seedAgents() { .values({ slug: "default", name: "默认助手", - description: "通用 AI 助手", + description: "通用 AI 助手,处理日常问答、信息查询、文本创作等通用任务。遇到专业编程问题时,可通过 agent-invoke 工具委托给编程助手处理。", systemPrompt: DEFAULT_SYSTEM_PROMPT, titleStrategy: "llm", enableThinking: 0, @@ -284,7 +301,7 @@ async function seedAgents() { .values({ slug: "coder", name: "编程助手", - description: "专注于编程问题的 AI 助手", + description: "专业编程助手,擅长代码编写、调试、代码审查、架构设计和技术方案评估。可处理多种编程语言(Python、TypeScript、Go、Rust 等),能主动使用 fetch、search 等工具查阅技术文档。当用户需要编写代码、排查 bug、做技术选型或审查代码质量时,应委托给此 Agent。", systemPrompt: CODER_SYSTEM_PROMPT, titleStrategy: "first-line", enableThinking: 1, diff --git a/server/service/agent-tool/executors/agent-invoke/agent-invoke.ts b/server/service/agent-tool/executors/agent-invoke/agent-invoke.ts new file mode 100644 index 0000000..15339f7 --- /dev/null +++ b/server/service/agent-tool/executors/agent-invoke/agent-invoke.ts @@ -0,0 +1,122 @@ +import { z } from "zod"; +import type { JSONSchema7 } from "json-schema"; +import type { ToolExecutor, ToolResult } from "../../registry"; +import type { AgentInvokeToolConfig } from "./config"; +import { invokeAgent } from "../../../agent/collaboration"; +import { listAgents } from "../../../agent/agent"; + +export const agentInvokeInputSchema = z.object({ + agentSlug: z.string().min(1).max(100).describe("目标 agent 的 slug"), + input: z.string().min(1).max(50000).describe("传递给目标 agent 的任务描述/输入内容"), +}); + +type AgentInvokeInput = z.infer; + +export const agentInvokeExecutor: ToolExecutor = { + buildInputSchema(_config: AgentInvokeToolConfig): JSONSchema7 { + return { + type: "object", + properties: { + agentSlug: { + type: "string", + description: "目标 agent 的 slug(唯一标识符)", + }, + input: { + type: "string", + description: "传递给目标 agent 的任务描述或输入内容", + }, + }, + required: ["agentSlug", "input"], + }; + }, + + buildDescription(_config: AgentInvokeToolConfig): string { + return "调用另一个 agent 来处理子任务。目标 agent 必须已启用且标记为可调用(isCallable)。传入 agent slug 和任务描述,返回目标 agent 的回复内容。支持多级调用链,但有递归深度限制。"; + }, + + async buildDescriptionAsync(_config: AgentInvokeToolConfig, ctx?: { agentId?: number | null }): Promise { + const base = this.buildDescription(_config); + const callableAgents = await listAgents(true); + const currentAgentId = ctx?.agentId ?? null; + const available = callableAgents.filter((a) => a.isCallable === 1 && a.id !== currentAgentId); + if (available.length === 0) { + return `${base}\n\n当前没有可调用的 Agent。请勿调用此工具。`; + } + const agentList = available.map((a) => `- slug: "${a.slug}" — ${a.name}: ${a.description ?? "无描述"}`).join("\n"); + return `${base}\n\n可调用的 Agent 列表(通过 agentSlug 参数指定):\n${agentList}`; + }, + + formatResult(result: ToolResult, _config: AgentInvokeToolConfig): string { + if (!result.success) { + return `Agent 调用失败: ${result.error ?? "未知错误"}`; + } + const meta = result.metadata; + const agentSlug = meta?.agentSlug ?? "unknown"; + const depth = meta?.recursionDepth ?? 0; + const duration = meta?.durationMs ?? 0; + const data = typeof result.data === "string" ? result.data : JSON.stringify(result.data); + return `[Agent "${agentSlug}" 调用完成 | 深度: ${depth} | 耗时: ${duration}ms]\n${data}`; + }, + + async execute( + input: unknown, + config: AgentInvokeToolConfig, + ctx: { userId: number | null; agentId?: number | null; recursionDepth?: number }, + ): Promise { + const start = Date.now(); + + const parsed = agentInvokeInputSchema.safeParse(input); + if (!parsed.success) { + return { + success: false, + data: null, + error: `参数校验失败: ${parsed.error.message}`, + metadata: { durationMs: Date.now() - start }, + }; + } + + const data: AgentInvokeInput = parsed.data; + const currentDepth = ctx.recursionDepth ?? 0; + + const result = await invokeAgent( + { + agentSlug: data.agentSlug, + input: data.input, + context: { + userId: ctx.userId, + callerAgentId: ctx.agentId ?? null, + recursionDepth: currentDepth + 1, + }, + }, + { + maxRecursionDepth: config.maxRecursionDepth, + timeoutMs: config.timeoutMs, + maxOutputTokens: config.maxOutputTokens, + }, + ); + + if (!result.ok) { + return { + success: false, + data: null, + error: result.error ?? `调用 agent ${data.agentSlug} 失败`, + metadata: { durationMs: Date.now() - start }, + }; + } + + let output = result.output; + if (output.length > config.resultTruncateLength) { + output = output.slice(0, config.resultTruncateLength) + `\n...[结果已截断,原始长度 ${output.length} 字符]`; + } + + return { + success: true, + data: output, + metadata: { + durationMs: Date.now() - start, + agentSlug: data.agentSlug, + recursionDepth: currentDepth + 1, + }, + }; + }, +}; diff --git a/server/service/agent-tool/executors/agent-invoke/config.ts b/server/service/agent-tool/executors/agent-invoke/config.ts new file mode 100644 index 0000000..d5519a4 --- /dev/null +++ b/server/service/agent-tool/executors/agent-invoke/config.ts @@ -0,0 +1,25 @@ +import { z } from "zod"; + +export const agentInvokeConfigSchema = z.object({ + maxRecursionDepth: z.number().int().positive().max(10).default(3), + resultTruncateLength: z.number().int().positive().max(50000).default(4000), + timeoutMs: z.number().int().positive().max(120000).default(30000), + maxOutputTokens: z.number().int().positive().max(32768).default(4096), +}); + +export type AgentInvokeToolConfig = z.infer; + +export const DEFAULT_AGENT_INVOKE_CONFIG: AgentInvokeToolConfig = { + maxRecursionDepth: 3, + resultTruncateLength: 8000, + timeoutMs: 60000, + maxOutputTokens: 8192, +}; + +export function parseAgentInvokeConfig(raw: unknown): AgentInvokeToolConfig { + const parsed = agentInvokeConfigSchema.safeParse(raw); + if (!parsed.success) { + throw new Error(`Invalid agent-invoke config: ${parsed.error.message}`); + } + return parsed.data; +} diff --git a/server/service/agent-tool/index.ts b/server/service/agent-tool/index.ts index 32f071e..e6cb195 100644 --- a/server/service/agent-tool/index.ts +++ b/server/service/agent-tool/index.ts @@ -38,6 +38,8 @@ import { parseMagic8ballConfig, DEFAULT_MAGIC8BALL_CONFIG } from "./executors/ma import { magic8ballExecutor, magic8ballInputSchema } from "./executors/magic-8ball/magic-8ball"; import { parseDocumentConfig, DEFAULT_DOCUMENT_CONFIG } from "./executors/document/config"; import { documentExecutor, documentInputSchema } from "./executors/document/document"; +import { parseAgentInvokeConfig, DEFAULT_AGENT_INVOKE_CONFIG } from "./executors/agent-invoke/config"; +import { agentInvokeExecutor, agentInvokeInputSchema } from "./executors/agent-invoke/agent-invoke"; // ============ 工具类型注册表 ============ interface ToolTypeRegistration { @@ -132,6 +134,12 @@ const TOOL_TYPE_REGISTRY: Record = { defaultConfig: DEFAULT_DOCUMENT_CONFIG, zodSchema: documentInputSchema, }, + "agent-invoke": { + executor: agentInvokeExecutor, + parseConfig: parseAgentInvokeConfig, + defaultConfig: DEFAULT_AGENT_INVOKE_CONFIG, + zodSchema: agentInvokeInputSchema, + }, }; // 立即注册所有工具类型 @@ -297,6 +305,7 @@ export async function executeAgentTool( userId: number | null, approved?: boolean, agentId?: number | null, + recursionDepth?: number, ): Promise { const agentTool = await getAgentToolById(id); if (!agentTool) { @@ -336,6 +345,7 @@ export async function executeAgentTool( toolSlug: agentTool.slug, userId, agentId: agentId ?? null, + recursionDepth: recursionDepth ?? 0, approved, }; @@ -510,7 +520,7 @@ export async function getAgentToolsForChat(params: { }, }), execute: async (input: unknown) => { - const execResult = await executeAgentTool(agentTool.id, input, userId, undefined, agentId); + const execResult = await executeAgentTool(agentTool.id, input, userId); if (!execResult.success) { return `工具执行失败: ${execResult.error ?? "未知错误"}。请停止调用此工具,基于已有信息回答用户或告知用户此工具不可用。`; } @@ -538,10 +548,11 @@ export async function getAgentToolsForChatByAgentId(params: { userId: number | null; userRole: UserRole | null; enableTools: boolean; + recursionDepth?: number; }): Promise<{ tools: Record>; }> { - const { agentId, userId, userRole, enableTools } = params; + const { agentId, userId, userRole, enableTools, recursionDepth } = params; if (!enableTools) { return { tools: {} }; @@ -591,8 +602,12 @@ export async function getAgentToolsForChatByAgentId(params: { ? row.associationNeedsApproval === 1 : agentTool.needsApproval === 1; + const description = executor.buildDescriptionAsync + ? await executor.buildDescriptionAsync(config, { agentId }) + : executor.buildDescription(config); + result[agentTool.slug] = tool({ - description: executor.buildDescription(config), + description, inputSchema: jsonSchema(jsonSch, { validate: (v: unknown) => { const r = zodSchema.safeParse(v); @@ -602,10 +617,13 @@ export async function getAgentToolsForChatByAgentId(params: { }, }), execute: async (input: unknown) => { - const execResult = await executeAgentTool(agentTool.id, input, userId, undefined, agentId); + const execResult = await executeAgentTool(agentTool.id, input, userId, undefined, agentId, recursionDepth); if (!execResult.success) { return `工具执行失败: ${execResult.error ?? "未知错误"}。请停止调用此工具,基于已有信息回答用户或告知用户此工具不可用。`; } + if (executor.formatResult) { + return executor.formatResult(execResult, config); + } const meta = execResult.metadata; const sizeInfo = meta?.responseSize ? `${meta.responseSize} bytes` : `未知大小`; const statusInfo = meta?.statusCode ? `HTTP ${meta.statusCode}` : ""; diff --git a/server/service/agent-tool/registry.ts b/server/service/agent-tool/registry.ts index e4a4c98..2ffe7ce 100644 --- a/server/service/agent-tool/registry.ts +++ b/server/service/agent-tool/registry.ts @@ -3,7 +3,9 @@ import type { JSONSchema7 } from "json-schema"; export interface ToolExecutor { buildInputSchema(config: TConfig): JSONSchema7; buildDescription(config: TConfig): string; + buildDescriptionAsync?(config: TConfig, ctx?: { agentId?: number | null }): Promise; execute(input: unknown, config: TConfig, ctx: ToolContext): Promise; + formatResult?(result: ToolResult, config: TConfig): string; } export interface ToolContext { @@ -11,6 +13,8 @@ export interface ToolContext { toolSlug: string; userId: number | null; approved?: boolean; + agentId?: number | null; + recursionDepth?: number; } export interface ToolResult { @@ -21,6 +25,7 @@ export interface ToolResult { statusCode?: number; responseSize?: number; durationMs: number; + [key: string]: unknown; }; } diff --git a/server/service/agent/chat-engine.ts b/server/service/agent/chat-engine.ts index ad0b09f..f9095f9 100644 --- a/server/service/agent/chat-engine.ts +++ b/server/service/agent/chat-engine.ts @@ -269,6 +269,7 @@ export async function executeChat(params: ChatEngineParams): Promise { +const DEFAULT_MAX_RECURSION_DEPTH = 3; +const DEFAULT_TIMEOUT_MS = 60_000; +const DEFAULT_MAX_OUTPUT_TOKENS = 8192; + +export async function invokeAgent( + invocation: AgentInvocation, + options?: { + maxRecursionDepth?: number; + timeoutMs?: number; + maxOutputTokens?: number; + }, +): Promise { const { agentSlug, input, context } = invocation; + const maxRecursionDepth = options?.maxRecursionDepth ?? DEFAULT_MAX_RECURSION_DEPTH; + const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS; + const maxOutputTokens = options?.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS; + + const userId = context?.userId ?? null; + const callerAgentId = context?.callerAgentId ?? null; + const recursionDepth = context?.recursionDepth ?? 0; const agent = await getAgentBySlug(agentSlug); if (!agent) { @@ -16,17 +37,79 @@ export async function invokeAgent(invocation: AgentInvocation): Promise= maxRecursionDepth) { + return { + agentSlug, + output: "", + ok: false, + error: `max recursion depth (${maxRecursionDepth}) reached`, + }; + } + + if (!agent.defaultModelId) { + return { agentSlug, output: "", ok: false, error: `agent ${agentSlug} has no defaultModelId` }; + } + + const resolved = await resolveModelForUser(agent.defaultModelId, userId); + if (!resolved) { + return { agentSlug, output: "", ok: false, error: `model ${agent.defaultModelId} not resolvable for agent ${agentSlug}` }; + } + + const languageModel = toLanguageModel(resolved); + + const enableTools = agent.enableTools === 1; + const { tools } = await getAgentToolsForChatByAgentId({ + agentId: agent.id, + userId, + userRole: null, + enableTools, + recursionDepth, + }); + + const maxSteps = agent.maxStepCount ?? 8; + + logger.info( + "[COLLAB] invokeAgent agentSlug=%s agentId=%d callerAgentId=%s recursionDepth=%d tools=%d maxSteps=%d", agentSlug, - input.length, - context ?? {}, + agent.id, + callerAgentId, + recursionDepth, + Object.keys(tools).length, + maxSteps, ); - return { - agentSlug, - output: "", - ok: false, - error: "agent orchestration not yet implemented", - }; + try { + const result = await generateText({ + model: languageModel, + system: agent.systemPrompt, + prompt: input, + ...(Object.keys(tools).length > 0 + ? { + tools, + stopWhen: stepCountIs(maxSteps), + } + : {}), + maxOutputTokens, + abortSignal: AbortSignal.timeout(timeoutMs), + }); + + const output = result.text ?? ""; + + logger.info( + "[COLLAB] invokeAgent done agentSlug=%s outputLen=%d usage=%j", + agentSlug, + output.length, + result.usage, + ); + + return { agentSlug, output, ok: true }; + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + logger.error("[COLLAB] invokeAgent error agentSlug=%s error=%s", agentSlug, errorMsg); + return { agentSlug, output: "", ok: false, error: errorMsg }; + } } diff --git a/server/service/agent/types.ts b/server/service/agent/types.ts index 6bde03f..d95dc6b 100644 --- a/server/service/agent/types.ts +++ b/server/service/agent/types.ts @@ -23,6 +23,8 @@ export interface AgentInvocation { context?: { sessionId?: string; userId?: number | null; + callerAgentId?: number | null; + recursionDepth?: number; }; }