plugin-agent-orchestrator 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = void 0;
7
+ var plugin_1 = require("./plugin");
8
+ Object.defineProperty(exports, "default", { enumerable: true, get: function () { return __importDefault(plugin_1).default; } });
@@ -0,0 +1,20 @@
1
+ {
2
+ "Agent Orchestrator": "Agent Orchestrator",
3
+ "Orchestration Rules": "Orchestration Rules",
4
+ "Swarm Tracing": "Swarm Tracing",
5
+ "Leader (Orchestrator)": "Leader (Orchestrator)",
6
+ "Sub-Agent": "Sub-Agent",
7
+ "Max Delegation Depth": "Max Delegation Depth",
8
+ "Timeout (ms)": "Timeout (ms)",
9
+ "Enabled": "Enabled",
10
+ "New Rule": "New Rule",
11
+ "Edit Orchestration Rule": "Edit Orchestration Rule",
12
+ "New Orchestration Rule": "New Orchestration Rule",
13
+ "Rule created": "Rule created",
14
+ "Rule updated": "Rule updated",
15
+ "Rule deleted": "Rule deleted",
16
+ "Sub-Agent Conversation": "Sub-Agent Conversation",
17
+ "Task Summary": "Task Summary",
18
+ "Parent Session": "Parent Session",
19
+ "No sub-agent executions yet": "No sub-agent executions yet"
20
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "Agent Orchestrator": "Điều phối Agent",
3
+ "Orchestration Rules": "Quy tắc điều phối",
4
+ "Swarm Tracing": "Truy vết Swarm",
5
+ "Leader (Orchestrator)": "Leader (Điều phối viên)",
6
+ "Sub-Agent": "Agent con",
7
+ "Max Delegation Depth": "Độ sâu ủy quyền tối đa",
8
+ "Timeout (ms)": "Thời gian chờ (ms)",
9
+ "Enabled": "Bật",
10
+ "New Rule": "Quy tắc mới",
11
+ "Edit Orchestration Rule": "Sửa quy tắc điều phối",
12
+ "New Orchestration Rule": "Quy tắc điều phối mới",
13
+ "Rule created": "Đã tạo quy tắc",
14
+ "Rule updated": "Đã cập nhật quy tắc",
15
+ "Rule deleted": "Đã xóa quy tắc",
16
+ "Sub-Agent Conversation": "Hội thoại Agent con",
17
+ "Task Summary": "Tóm tắt nhiệm vụ",
18
+ "Parent Session": "Phiên gốc",
19
+ "No sub-agent executions yet": "Chưa có lượt thực thi agent con nào"
20
+ }
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PluginAgentOrchestratorClient = void 0;
4
+ const client_1 = require("@nocobase/client");
5
+ const OrchestratorSettings_1 = require("./OrchestratorSettings");
6
+ class PluginAgentOrchestratorClient extends client_1.Plugin {
7
+ async load() {
8
+ // Register under the "AI" settings group for consistency with other AI plugins
9
+ this.app.pluginSettingsManager.add('ai.orchestrator', {
10
+ title: 'Agent Orchestrator',
11
+ icon: 'ApartmentOutlined',
12
+ Component: OrchestratorSettings_1.OrchestratorSettings,
13
+ });
14
+ }
15
+ }
16
+ exports.PluginAgentOrchestratorClient = PluginAgentOrchestratorClient;
17
+ exports.default = PluginAgentOrchestratorClient;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = void 0;
7
+ var plugin_1 = require("./plugin");
8
+ Object.defineProperty(exports, "default", { enumerable: true, get: function () { return __importDefault(plugin_1).default; } });
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PluginAgentOrchestratorServer = void 0;
7
+ const server_1 = require("@nocobase/server");
8
+ const path_1 = __importDefault(require("path"));
9
+ const delegate_task_1 = require("./tools/delegate-task");
10
+ const tracing_1 = require("./resources/tracing");
11
+ class PluginAgentOrchestratorServer extends server_1.Plugin {
12
+ async afterAdd() { }
13
+ async beforeLoad() {
14
+ // Import collection definitions
15
+ this.db.import({ directory: path_1.default.resolve(__dirname, 'collections') });
16
+ }
17
+ async load() {
18
+ // --- ACL ---
19
+ this.app.acl.registerSnippet({
20
+ name: `pm.${this.name}`,
21
+ actions: ['orchestratorConfig:*'],
22
+ });
23
+ // --- Register Dynamic Tools ---
24
+ // Each configured sub-agent becomes a callable tool for its leader.
25
+ // Uses createReactAgent (LangGraph public API) instead of private AIEmployee class.
26
+ // Tools are registered via app.aiManager.toolsManager (public API from @nocobase/ai core).
27
+ const toolsManager = this.app.aiManager.toolsManager;
28
+ toolsManager.registerDynamicTools((0, delegate_task_1.createDelegateToolsProvider)(this));
29
+ // --- Register Tracing Resource (Phase 5) ---
30
+ // Custom read-only resource for the Swarm Tracing admin page.
31
+ (0, tracing_1.registerTracingResource)(this);
32
+ // NOTE: The createReactAgent approach does NOT create aiConversation records,
33
+ // so there is no need for a middleware to hide "headless" conversations.
34
+ // If future versions need conversation logging, add it here.
35
+ }
36
+ async install() {
37
+ // No seed data needed on first install
38
+ }
39
+ async afterEnable() { }
40
+ async afterDisable() { }
41
+ async remove() { }
42
+ }
43
+ exports.PluginAgentOrchestratorServer = PluginAgentOrchestratorServer;
44
+ exports.default = PluginAgentOrchestratorServer;
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerTracingResource = void 0;
4
+ /**
5
+ * Custom resource for the Swarm Tracing admin UI (Phase 5).
6
+ * Queries the dedicated orchestratorLogs collection instead of
7
+ * filtering aiConversations by JSONB (P2 fix: DB-engine agnostic).
8
+ */
9
+ function registerTracingResource(plugin) {
10
+ const app = plugin.app;
11
+ app.resource({
12
+ name: 'orchestratorTracing',
13
+ actions: {
14
+ /**
15
+ * List all delegation execution logs.
16
+ */
17
+ async list(ctx, next) {
18
+ const repo = ctx.db.getRepository('orchestratorLogs');
19
+ const { page = 1, pageSize = 50, sort = ['-createdAt'] } = ctx.action.params;
20
+ try {
21
+ const [rows, count] = await repo.findAndCount({
22
+ sort,
23
+ offset: (Number(page) - 1) * Number(pageSize),
24
+ limit: Number(pageSize),
25
+ });
26
+ ctx.body = {
27
+ data: rows.map((row) => ({
28
+ id: row.id,
29
+ leaderUsername: row.leaderUsername,
30
+ subAgentUsername: row.subAgentUsername,
31
+ toolName: row.toolName,
32
+ task: row.task,
33
+ result: row.result,
34
+ status: row.status,
35
+ depth: row.depth,
36
+ durationMs: row.durationMs,
37
+ error: row.error,
38
+ userId: row.userId,
39
+ createdAt: row.createdAt,
40
+ })),
41
+ meta: {
42
+ count,
43
+ page: Number(page),
44
+ pageSize: Number(pageSize),
45
+ totalPage: Math.ceil(count / Number(pageSize)),
46
+ },
47
+ };
48
+ }
49
+ catch (e) {
50
+ ctx.log.error('[AgentOrchestrator] Tracing list error', e);
51
+ ctx.body = { data: [], meta: { count: 0 } };
52
+ }
53
+ await next();
54
+ },
55
+ /**
56
+ * Get a single delegation log by ID.
57
+ */
58
+ async get(ctx, next) {
59
+ const { filterByTk } = ctx.action.params;
60
+ if (!filterByTk) {
61
+ ctx.throw(400, 'id is required');
62
+ return;
63
+ }
64
+ try {
65
+ const repo = ctx.db.getRepository('orchestratorLogs');
66
+ const log = await repo.findOne({
67
+ filter: { id: filterByTk },
68
+ });
69
+ ctx.body = { data: log };
70
+ }
71
+ catch (e) {
72
+ ctx.log.error('[AgentOrchestrator] Tracing get error', e);
73
+ ctx.body = { data: null };
74
+ }
75
+ await next();
76
+ },
77
+ },
78
+ });
79
+ // ACL: allow admin access to tracing endpoints
80
+ app.acl.registerSnippet({
81
+ name: `pm.${plugin.name}.tracing`,
82
+ actions: ['orchestratorTracing:list', 'orchestratorTracing:get'],
83
+ });
84
+ }
85
+ exports.registerTracingResource = registerTracingResource;
@@ -0,0 +1,317 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDelegateToolsProvider = void 0;
4
+ const zod_1 = require("zod");
5
+ // @ts-ignore - subpath export types resolve at build time via NocoBase bundler
6
+ const prebuilt_1 = require("@langchain/langgraph/prebuilt");
7
+ const tools_1 = require("@langchain/core/tools");
8
+ const messages_1 = require("@langchain/core/messages");
9
+ /**
10
+ * Maximum delegation depth key stored in ctx metadata.
11
+ * Used to prevent circular/recursive delegation chains.
12
+ */
13
+ const ORCHESTRATOR_DEPTH_KEY = '__orchestratorDepth';
14
+ /**
15
+ * Creates one dynamic tool per configured sub-agent for a given leader.
16
+ * Uses Strategy B (Per-SubAgent Tool): each sub-agent becomes a separate tool
17
+ * with its own name and description, making LLM tool selection natural.
18
+ *
19
+ * Architecture:
20
+ * - Uses createReactAgent (public LangGraph API) for agent execution
21
+ * - Uses plugin-ai's getLLMService() for LLM model resolution
22
+ * - Uses core app.aiManager.toolsManager.listTools() for tool resolution
23
+ * (same manager that AIEmployee uses — see ai-employee.ts:1286)
24
+ * - Depth enforcement via ctx metadata tracking
25
+ * - Per-leader scoping via invoke-time check (core ToolsOptions has no
26
+ * leaderUsername field, so scoping is enforced in the invoke callback)
27
+ */
28
+ function createDelegateToolsProvider(plugin) {
29
+ return async (register) => {
30
+ try {
31
+ const configRepo = plugin.db.getRepository('orchestratorConfig');
32
+ if (!configRepo)
33
+ return;
34
+ const configs = await configRepo.find({
35
+ filter: { enabled: true },
36
+ });
37
+ if (!configs?.length)
38
+ return;
39
+ // Build a lookup: which leaders are allowed to use each delegate tool
40
+ // Multiple leaders may share the same sub-agent, but each leader must
41
+ // have an explicit rule.
42
+ const leadersByTool = new Map();
43
+ for (const config of configs) {
44
+ const toolName = `delegate_to_${config.subAgentUsername.replace(/[^a-zA-Z0-9_-]/g, '_')}`;
45
+ if (!leadersByTool.has(toolName)) {
46
+ leadersByTool.set(toolName, new Set());
47
+ }
48
+ leadersByTool.get(toolName).add(config.leaderUsername);
49
+ }
50
+ // De-duplicate: register one tool per sub-agent (not per config row)
51
+ const seenSubAgents = new Set();
52
+ for (const config of configs) {
53
+ const { leaderUsername, subAgentUsername, maxDepth, timeout } = config;
54
+ // Skip if we already registered this sub-agent's tool
55
+ if (seenSubAgents.has(subAgentUsername))
56
+ continue;
57
+ seenSubAgents.add(subAgentUsername);
58
+ // Fetch the sub-agent employee model for its description and LLM config
59
+ const subAgentEmployee = await plugin.db.getRepository('aiEmployees').findOne({
60
+ filter: { username: subAgentUsername },
61
+ });
62
+ if (!subAgentEmployee)
63
+ continue;
64
+ const toolName = `delegate_to_${subAgentUsername.replace(/[^a-zA-Z0-9_-]/g, '_')}`;
65
+ const toolDescription = [
66
+ `Delegate a task to the AI Employee "${subAgentEmployee.nickname || subAgentUsername}".`,
67
+ subAgentEmployee.about ? `Specialist profile: ${subAgentEmployee.about.substring(0, 200)}` : '',
68
+ 'The sub-agent will execute the task independently and return its final answer.',
69
+ ].filter(Boolean).join(' ');
70
+ // Capture the allowed leaders for this tool (for invoke-time scoping)
71
+ const allowedLeaders = leadersByTool.get(toolName);
72
+ register.registerTools({
73
+ scope: 'CUSTOM',
74
+ execution: 'backend',
75
+ defaultPermission: 'ALLOW',
76
+ silence: false,
77
+ introduction: {
78
+ title: `[Sub-Agent] ${subAgentEmployee.nickname || subAgentUsername}`,
79
+ about: toolDescription,
80
+ },
81
+ definition: {
82
+ name: toolName,
83
+ description: toolDescription,
84
+ schema: zod_1.z.object({
85
+ task: zod_1.z.string().describe('The detailed task description for the sub-agent to execute.'),
86
+ context: zod_1.z.string().optional().describe('Optional additional context to help the sub-agent understand the task better.'),
87
+ }),
88
+ },
89
+ invoke: async (ctx, args, id) => {
90
+ // --- P2 FIX: Per-leader scoping at invoke time ---
91
+ // Core ToolsOptions doesn't support leaderUsername field, so
92
+ // we enforce scoping here by checking the calling employee.
93
+ const callingEmployee = ctx._currentAIEmployee?.username
94
+ || ctx.state?.currentAIEmployee;
95
+ if (callingEmployee && !allowedLeaders.has(callingEmployee)) {
96
+ return {
97
+ status: 'error',
98
+ content: `Employee "${callingEmployee}" is not authorized to delegate to "${subAgentUsername}". Configure an orchestration rule first.`,
99
+ };
100
+ }
101
+ return invokeDelegateTask(ctx, plugin, {
102
+ leaderUsername: callingEmployee || Array.from(allowedLeaders)[0] || '',
103
+ subAgentUsername,
104
+ subAgentEmployee,
105
+ task: args.task,
106
+ context: args.context,
107
+ maxDepth: maxDepth ?? 1,
108
+ timeout: timeout ?? 120000,
109
+ toolCallId: id,
110
+ });
111
+ },
112
+ });
113
+ }
114
+ }
115
+ catch (e) {
116
+ plugin.app.log.error('[AgentOrchestrator] Failed to register delegate tools', e);
117
+ }
118
+ };
119
+ }
120
+ exports.createDelegateToolsProvider = createDelegateToolsProvider;
121
+ /**
122
+ * Core execution logic using createReactAgent (public LangGraph API).
123
+ *
124
+ * This approach mirrors plugin-sub-agent's proven pattern:
125
+ * 1. Get LLM model via aiPlugin.aiManager.getLLMService()
126
+ * 2. Resolve sub-agent's tools via core app.aiManager.toolsManager.listTools()
127
+ * 3. Build a standalone createReactAgent with the model + tools
128
+ * 4. Stream results and extract final AI message
129
+ *
130
+ * Tool resolution uses the CORE toolsManager (app.aiManager.toolsManager) —
131
+ * the same manager that AIEmployee.getToolsMap() uses (see ai-employee.ts:1286).
132
+ * This ensures tool names in skillSettings.skills[].name match correctly.
133
+ *
134
+ * skillSettings.skills shape (verified against ai-employee.ts:1028):
135
+ * { name: string, autoCall: boolean }[]
136
+ */
137
+ async function invokeDelegateTask(ctx, plugin, options) {
138
+ const { leaderUsername, subAgentUsername, subAgentEmployee, task, context, maxDepth, timeout, toolCallId } = options;
139
+ // --- P1: Depth enforcement ---
140
+ const currentDepth = ctx[ORCHESTRATOR_DEPTH_KEY] ?? 0;
141
+ if (currentDepth >= maxDepth) {
142
+ return {
143
+ status: 'error',
144
+ content: `Delegation depth limit reached (${currentDepth}/${maxDepth}). Sub-agent "${subAgentUsername}" cannot delegate further.`,
145
+ };
146
+ }
147
+ const startTime = Date.now();
148
+ try {
149
+ const aiPlugin = ctx.app.pm.get('ai');
150
+ if (!aiPlugin) {
151
+ throw new Error('Plugin AI is not installed or enabled');
152
+ }
153
+ // --- Step 1: Resolve LLM model from sub-agent's employee config ---
154
+ const modelSettings = subAgentEmployee.modelSettings;
155
+ if (!modelSettings?.llmService || !modelSettings?.model) {
156
+ throw new Error(`Sub-agent "${subAgentUsername}" has no LLM model configured. Please configure a model in the AI Employee settings.`);
157
+ }
158
+ const { provider } = await aiPlugin.aiManager.getLLMService({
159
+ llmService: modelSettings.llmService,
160
+ model: modelSettings.model,
161
+ });
162
+ const chatModel = provider.createModel();
163
+ // --- Step 2: Resolve tools via CORE toolsManager ---
164
+ // Uses app.aiManager.toolsManager (same as AIEmployee.getToolsMap at ai-employee.ts:1286)
165
+ // NOT plugin-ai's local toolManager (which has a different grouped format).
166
+ const coreToolsManager = ctx.app.aiManager.toolsManager;
167
+ const allTools = await coreToolsManager.listTools();
168
+ // skillSettings.skills is { name: string, autoCall: boolean }[]
169
+ // (verified at ai-employee.ts:1028-1029)
170
+ const employeeSkills = (subAgentEmployee.skillSettings?.skills ?? [])
171
+ .map((s) => (typeof s === 'string' ? s : s.name))
172
+ .filter(Boolean);
173
+ const langchainTools = [];
174
+ for (const toolEntry of allTools) {
175
+ const toolName = toolEntry.definition.name;
176
+ if (!toolName)
177
+ continue;
178
+ // Only include tools that the sub-agent employee is configured to use.
179
+ // Also skip our own delegate_to_* tools to prevent circular delegation
180
+ // (belt-and-suspenders with the depth check above).
181
+ if (!employeeSkills.includes(toolName) || toolName.startsWith('delegate_to_')) {
182
+ continue;
183
+ }
184
+ langchainTools.push(new tools_1.DynamicStructuredTool({
185
+ name: toolName.replace(/[^a-zA-Z0-9_-]/g, '_'),
186
+ description: toolEntry.definition.description || toolName,
187
+ schema: (toolEntry.definition.schema || zod_1.z.object({})),
188
+ func: async (toolArgs) => {
189
+ // Forward the invoke with depth tracking
190
+ const invokeCtx = Object.create(ctx);
191
+ invokeCtx[ORCHESTRATOR_DEPTH_KEY] = currentDepth + 1;
192
+ const res = await toolEntry.invoke(invokeCtx, toolArgs, `orch-${toolCallId}`);
193
+ if (res?.status === 'error') {
194
+ throw new Error(`Tool <${toolName}> failed: ${res.content}`);
195
+ }
196
+ return typeof res?.content === 'string' ? res.content : JSON.stringify(res);
197
+ },
198
+ }));
199
+ }
200
+ // --- Step 3: Build the agent ---
201
+ const abortController = new AbortController();
202
+ const executor = (0, prebuilt_1.createReactAgent)({
203
+ llm: chatModel,
204
+ tools: langchainTools,
205
+ });
206
+ // --- Step 4: Construct messages ---
207
+ const systemPrompt = subAgentEmployee.chatSettings?.systemPrompt
208
+ || subAgentEmployee.bio
209
+ || `You are an AI assistant named "${subAgentEmployee.nickname || subAgentUsername}". ${subAgentEmployee.about || ''}`;
210
+ const combinedTask = context
211
+ ? `Task: ${task}\n\nContext Provided:\n${context}`
212
+ : `Task: ${task}`;
213
+ // --- Step 5: Execute with timeout + abort ---
214
+ // P3 FIX: AbortController signal cancels the in-flight stream on timeout,
215
+ // preventing continued token consumption after the timeout fires.
216
+ const invokePromise = executeAgent(executor, systemPrompt, combinedTask, abortController.signal);
217
+ const result = await Promise.race([
218
+ invokePromise,
219
+ createTimeout(timeout, subAgentUsername, abortController),
220
+ ]);
221
+ const content = result || 'Sub-agent completed the task but produced no output.';
222
+ // Log successful execution for tracing
223
+ await logDelegation(ctx, plugin, {
224
+ leaderUsername,
225
+ subAgentUsername,
226
+ task,
227
+ result: content,
228
+ status: 'success',
229
+ depth: currentDepth,
230
+ durationMs: Date.now() - startTime,
231
+ });
232
+ return {
233
+ status: 'success',
234
+ content,
235
+ };
236
+ }
237
+ catch (e) {
238
+ plugin.app.log.error(`[AgentOrchestrator] Sub-agent ${subAgentUsername} failed`, e);
239
+ // Log failed execution for tracing
240
+ await logDelegation(ctx, plugin, {
241
+ leaderUsername,
242
+ subAgentUsername,
243
+ task,
244
+ result: '',
245
+ status: 'error',
246
+ depth: currentDepth,
247
+ durationMs: Date.now() - startTime,
248
+ error: e.message,
249
+ }).catch(() => { }); // Don't let logging errors mask the real error
250
+ return {
251
+ status: 'error',
252
+ content: `Sub-agent "${subAgentUsername}" failed: ${e.message}`,
253
+ };
254
+ }
255
+ }
256
+ /**
257
+ * Log a delegation event to the orchestratorLogs collection for observability.
258
+ */
259
+ async function logDelegation(ctx, plugin, data) {
260
+ try {
261
+ const logsRepo = plugin.db.getRepository('orchestratorLogs');
262
+ if (!logsRepo)
263
+ return;
264
+ await logsRepo.create({
265
+ values: {
266
+ leaderUsername: data.leaderUsername,
267
+ subAgentUsername: data.subAgentUsername,
268
+ toolName: `delegate_to_${data.subAgentUsername}`,
269
+ task: data.task.substring(0, 2000),
270
+ result: data.result.substring(0, 5000),
271
+ status: data.status,
272
+ depth: data.depth,
273
+ durationMs: data.durationMs,
274
+ error: data.error?.substring(0, 2000),
275
+ userId: ctx.auth?.user?.id || ctx.state?.currentUser?.id,
276
+ },
277
+ });
278
+ }
279
+ catch (e) {
280
+ plugin.app.log.warn('[AgentOrchestrator] Failed to log delegation event', e);
281
+ }
282
+ }
283
+ /**
284
+ * Execute the agent and extract the final AI message content.
285
+ * Uses stream mode to collect all chunks, similar to plugin-sub-agent.
286
+ * Accepts an AbortSignal so the stream can be cancelled on timeout.
287
+ */
288
+ async function executeAgent(executor, systemPrompt, task, signal) {
289
+ const streamOptions = { recursionLimit: 50, streamMode: 'messages' };
290
+ if (signal) {
291
+ streamOptions.signal = signal;
292
+ }
293
+ const stream = await executor.stream({
294
+ messages: [new messages_1.SystemMessage(systemPrompt), new messages_1.HumanMessage(task)],
295
+ }, streamOptions);
296
+ let aiContentCache = '';
297
+ for await (const chunk of stream) {
298
+ // Check abort between chunks
299
+ if (signal?.aborted)
300
+ break;
301
+ const [message] = chunk;
302
+ if (message.getType() === 'ai' && message.content) {
303
+ aiContentCache += message.content.toString();
304
+ }
305
+ }
306
+ return aiContentCache || '';
307
+ }
308
+ /**
309
+ * Create a timeout promise that rejects after the specified duration.
310
+ * P3 FIX: Also triggers AbortController to cancel the in-flight stream.
311
+ */
312
+ function createTimeout(ms, agentName, abortController) {
313
+ return new Promise((_, reject) => setTimeout(() => {
314
+ abortController?.abort();
315
+ reject(new Error(`Sub-agent "${agentName}" timed out after ${ms / 1000}s`));
316
+ }, ms));
317
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "plugin-agent-orchestrator",
3
+ "displayName": "Agent Orchestrator",
4
+ "displayName.zh-CN": "代理协调器",
5
+ "displayName.vi-VN": "Điều phối Agent",
6
+ "description": "Hierarchical Multi-Agent orchestration for NocoBase AI Employees. Enables Leader agents to delegate tasks to Sub-Agent employees without modifying core plugin-ai.",
7
+ "version": "1.0.0",
8
+ "license": "Apache-2.0",
9
+ "main": "dist/server/index.js",
10
+ "keywords": [
11
+ "AI",
12
+ "Agent",
13
+ "Orchestrator",
14
+ "Multi-Agent",
15
+ "Swarm"
16
+ ],
17
+ "files": [
18
+ "dist",
19
+ "src",
20
+ "client.js",
21
+ "server.js",
22
+ "client.d.ts",
23
+ "server.d.ts"
24
+ ],
25
+ "nocobase": {
26
+ "supportedVersions": [
27
+ "2.x"
28
+ ],
29
+ "editionLevel": 0
30
+ },
31
+ "dependencies": {
32
+ "@langchain/core": "^0.3.0",
33
+ "@langchain/langgraph": "^0.2.0",
34
+ "zod": "^3.23.0"
35
+ },
36
+ "peerDependencies": {
37
+ "@nocobase/client": "2.x",
38
+ "@nocobase/server": "2.x",
39
+ "@nocobase/database": "2.x",
40
+ "@nocobase/ai": "2.x",
41
+ "@nocobase/plugin-ai": "2.x"
42
+ }
43
+ }
package/server.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './dist/server';
2
+ export { default } from './dist/server';
package/server.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./dist/server');
@@ -0,0 +1,49 @@
1
+ import React from 'react';
2
+ import { Select } from 'antd';
3
+ import { useAIEmployees } from './AIEmployeesContext';
4
+
5
+ /**
6
+ * Reusable Select component for AI Employees.
7
+ * P3 FIX: Uses shared AIEmployeesContext instead of making its own API call.
8
+ */
9
+ export const AIEmployeeSelect: React.FC<{
10
+ value?: string;
11
+ onChange?: (value: string) => void;
12
+ exclude?: string; // username to exclude (prevent self-reference)
13
+ placeholder?: string;
14
+ }> = ({ value, onChange, exclude, placeholder = 'Select AI Employee...' }) => {
15
+ const { employees, loading } = useAIEmployees();
16
+
17
+ const options = React.useMemo(() => {
18
+ return employees
19
+ .filter((emp) => !exclude || emp.username !== exclude)
20
+ .map((emp) => ({
21
+ label: emp.nickname,
22
+ value: emp.username,
23
+ description: emp.about,
24
+ }));
25
+ }, [employees, exclude]);
26
+
27
+ return (
28
+ <Select
29
+ loading={loading}
30
+ options={options}
31
+ value={value}
32
+ onChange={onChange}
33
+ placeholder={placeholder}
34
+ showSearch
35
+ filterOption={(input, option) =>
36
+ (option?.label ?? '').toString().toLowerCase().includes(input.toLowerCase()) ||
37
+ (option?.value ?? '').toString().toLowerCase().includes(input.toLowerCase())
38
+ }
39
+ optionRender={(option) => (
40
+ <div>
41
+ <div style={{ fontWeight: 500 }}>{option.label}</div>
42
+ {option.data.description && (
43
+ <div style={{ fontSize: 12, color: '#888' }}>{option.data.description}</div>
44
+ )}
45
+ </div>
46
+ )}
47
+ />
48
+ );
49
+ };