Browse Source

feat: 添加 agent-invoke 工具,支持多 Agent 协作与调用

feat/agent-independence
npmrun 2 months ago
parent
commit
29a8b1a0be
  1. BIN
      packages/drizzle-pkg/db.sqlite
  2. 21
      packages/drizzle-pkg/seed.ts
  3. 122
      server/service/agent-tool/executors/agent-invoke/agent-invoke.ts
  4. 25
      server/service/agent-tool/executors/agent-invoke/config.ts
  5. 26
      server/service/agent-tool/index.ts
  6. 5
      server/service/agent-tool/registry.ts
  7. 1
      server/service/agent/chat-engine.ts
  8. 105
      server/service/agent/collaboration.ts
  9. 2
      server/service/agent/types.ts

BIN
packages/drizzle-pkg/db.sqlite

Binary file not shown.

21
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,

122
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<typeof agentInvokeInputSchema>;
export const agentInvokeExecutor: ToolExecutor<AgentInvokeToolConfig> = {
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<string> {
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<ToolResult> {
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,
},
};
},
};

25
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<typeof agentInvokeConfigSchema>;
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;
}

26
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<string, ToolTypeRegistration> = {
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<ToolResult> {
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<string, ReturnType<typeof tool>>;
}> {
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}` : "";

5
server/service/agent-tool/registry.ts

@ -3,7 +3,9 @@ import type { JSONSchema7 } from "json-schema";
export interface ToolExecutor<TConfig> {
buildInputSchema(config: TConfig): JSONSchema7;
buildDescription(config: TConfig): string;
buildDescriptionAsync?(config: TConfig, ctx?: { agentId?: number | null }): Promise<string>;
execute(input: unknown, config: TConfig, ctx: ToolContext): Promise<ToolResult>;
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;
};
}

1
server/service/agent/chat-engine.ts

@ -269,6 +269,7 @@ export async function executeChat(params: ChatEngineParams): Promise<ChatEngineR
userId: user?.id ?? null,
userRole: (user?.role as any) ?? null,
enableTools: toolsEnabled,
recursionDepth: 0,
});
let historyMessages = await getMessagesBySession(sessionId, { limit: 100, latest: true });

105
server/service/agent/collaboration.ts

@ -1,11 +1,32 @@
import { generateText, stepCountIs } from "ai";
import type { AgentInvocation, AgentInvocationResult } from "./types";
import { getAgentBySlug } from "./agent";
import { getAgentToolsForChatByAgentId } from "#server/service/agent-tool";
import { resolveModelForUser, toLanguageModel } from "#server/service/llm/model-resolver";
import log4js from "logger";
const logger = log4js.getLogger("APP");
export async function invokeAgent(invocation: AgentInvocation): Promise<AgentInvocationResult> {
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<AgentInvocationResult> {
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<AgentInv
return { agentSlug, output: "", ok: false, error: `agent not callable: ${agentSlug}` };
}
logger.warn(
"[COLLAB] invokeAgent stub called agentSlug=%s inputLen=%d context=%j — orchestration not yet implemented",
if (callerAgentId !== null && callerAgentId === agent.id) {
return { agentSlug, output: "", ok: false, error: `self-invocation blocked: agent ${agentSlug} cannot invoke itself` };
}
if (recursionDepth >= 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 };
}
}

2
server/service/agent/types.ts

@ -23,6 +23,8 @@ export interface AgentInvocation {
context?: {
sessionId?: string;
userId?: number | null;
callerAgentId?: number | null;
recursionDepth?: number;
};
}

Loading…
Cancel
Save