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.
- package/README.md +291 -0
- package/client.d.ts +2 -0
- package/client.js +1 -0
- package/dist/client/AIEmployeeSelect.js +29 -0
- package/dist/client/AIEmployeesContext.js +64 -0
- package/dist/client/OrchestratorSettings.js +32 -0
- package/dist/client/RulesTab.js +144 -0
- package/dist/client/TracingTab.js +88 -0
- package/dist/client/index.js +8 -0
- package/dist/client/locale/en-US.json +20 -0
- package/dist/client/locale/vi-VN.json +20 -0
- package/dist/client/plugin.js +17 -0
- package/dist/server/index.js +8 -0
- package/dist/server/plugin.js +44 -0
- package/dist/server/resources/tracing.js +85 -0
- package/dist/server/tools/delegate-task.js +317 -0
- package/package.json +43 -0
- package/server.d.ts +2 -0
- package/server.js +1 -0
- package/src/client/AIEmployeeSelect.tsx +49 -0
- package/src/client/AIEmployeesContext.tsx +58 -0
- package/src/client/OrchestratorSettings.tsx +46 -0
- package/src/client/RulesTab.tsx +272 -0
- package/src/client/TracingTab.tsx +227 -0
- package/src/client/index.tsx +1 -0
- package/src/client/plugin.tsx +15 -0
- package/src/index.ts +2 -0
- package/src/locale/en-US.json +20 -0
- package/src/locale/vi-VN.json +20 -0
- package/src/server/collections/orchestrator-config.ts +49 -0
- package/src/server/collections/orchestrator-logs.ts +99 -0
- package/src/server/index.ts +1 -0
- package/src/server/plugin.ts +46 -0
- package/src/server/resources/tracing.ts +91 -0
- package/src/server/tools/delegate-task.ts +388 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { defineCollection } from '@nocobase/database';
|
|
2
|
+
|
|
3
|
+
export default defineCollection({
|
|
4
|
+
name: 'orchestratorConfig',
|
|
5
|
+
title: 'Orchestrator Config',
|
|
6
|
+
fields: [
|
|
7
|
+
{
|
|
8
|
+
name: 'id',
|
|
9
|
+
type: 'bigInt',
|
|
10
|
+
autoIncrement: true,
|
|
11
|
+
primaryKey: true,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: 'leaderUsername',
|
|
15
|
+
type: 'string',
|
|
16
|
+
allowNull: false,
|
|
17
|
+
comment: 'AI Employee username that acts as the Leader/Orchestrator',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'subAgentUsername',
|
|
21
|
+
type: 'string',
|
|
22
|
+
allowNull: false,
|
|
23
|
+
comment: 'AI Employee username that acts as the Sub-Agent',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'enabled',
|
|
27
|
+
type: 'boolean',
|
|
28
|
+
defaultValue: true,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'maxDepth',
|
|
32
|
+
type: 'integer',
|
|
33
|
+
defaultValue: 1,
|
|
34
|
+
comment: 'Maximum delegation depth (1 = leader can call sub, sub cannot call further)',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'timeout',
|
|
38
|
+
type: 'integer',
|
|
39
|
+
defaultValue: 120000,
|
|
40
|
+
comment: 'Timeout in ms for sub-agent execution',
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
indexes: [
|
|
44
|
+
{
|
|
45
|
+
unique: true,
|
|
46
|
+
fields: ['leaderUsername', 'subAgentUsername'],
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { defineCollection } from '@nocobase/database';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stores delegation execution logs for Swarm Tracing (Phase 5).
|
|
5
|
+
*
|
|
6
|
+
* Since createReactAgent doesn't create aiConversation records,
|
|
7
|
+
* we log delegation events to a dedicated table for observability.
|
|
8
|
+
*/
|
|
9
|
+
export default defineCollection({
|
|
10
|
+
name: 'orchestratorLogs',
|
|
11
|
+
title: 'Orchestrator Logs',
|
|
12
|
+
fields: [
|
|
13
|
+
{
|
|
14
|
+
name: 'id',
|
|
15
|
+
type: 'bigInt',
|
|
16
|
+
autoIncrement: true,
|
|
17
|
+
primaryKey: true,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'leaderUsername',
|
|
21
|
+
type: 'string',
|
|
22
|
+
allowNull: false,
|
|
23
|
+
comment: 'The AI Employee that initiated the delegation',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'subAgentUsername',
|
|
27
|
+
type: 'string',
|
|
28
|
+
allowNull: false,
|
|
29
|
+
comment: 'The AI Employee that executed the delegated task',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'toolName',
|
|
33
|
+
type: 'string',
|
|
34
|
+
comment: 'The tool name used for delegation (e.g., delegate_to_sql_expert)',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'task',
|
|
38
|
+
type: 'text',
|
|
39
|
+
comment: 'The task description sent to the sub-agent',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'result',
|
|
43
|
+
type: 'text',
|
|
44
|
+
comment: 'The sub-agent result content',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'status',
|
|
48
|
+
type: 'string',
|
|
49
|
+
comment: 'success or error',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'depth',
|
|
53
|
+
type: 'integer',
|
|
54
|
+
defaultValue: 0,
|
|
55
|
+
comment: 'Delegation depth level (0 = first-level delegation)',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'durationMs',
|
|
59
|
+
type: 'integer',
|
|
60
|
+
comment: 'Execution duration in milliseconds',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'error',
|
|
64
|
+
type: 'text',
|
|
65
|
+
comment: 'Error message if status is error',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'userId',
|
|
69
|
+
type: 'bigInt',
|
|
70
|
+
comment: 'The user who triggered the leader conversation',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'createdAt',
|
|
74
|
+
type: 'date',
|
|
75
|
+
interface: 'createdAt',
|
|
76
|
+
field: 'createdAt',
|
|
77
|
+
uiSchema: {
|
|
78
|
+
type: 'datetime',
|
|
79
|
+
title: '{{t("Created at")}}',
|
|
80
|
+
'x-component': 'DatePicker',
|
|
81
|
+
'x-component-props': { showTime: true },
|
|
82
|
+
'x-read-pretty': true,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'updatedAt',
|
|
87
|
+
type: 'date',
|
|
88
|
+
interface: 'updatedAt',
|
|
89
|
+
field: 'updatedAt',
|
|
90
|
+
uiSchema: {
|
|
91
|
+
type: 'datetime',
|
|
92
|
+
title: '{{t("Updated at")}}',
|
|
93
|
+
'x-component': 'DatePicker',
|
|
94
|
+
'x-component-props': { showTime: true },
|
|
95
|
+
'x-read-pretty': true,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './plugin';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Plugin } from '@nocobase/server';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { createDelegateToolsProvider } from './tools/delegate-task';
|
|
4
|
+
import { registerTracingResource } from './resources/tracing';
|
|
5
|
+
|
|
6
|
+
export class PluginAgentOrchestratorServer extends Plugin {
|
|
7
|
+
async afterAdd() {}
|
|
8
|
+
|
|
9
|
+
async beforeLoad() {
|
|
10
|
+
// Import collection definitions
|
|
11
|
+
this.db.import({ directory: path.resolve(__dirname, 'collections') });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async load() {
|
|
15
|
+
// --- ACL ---
|
|
16
|
+
this.app.acl.registerSnippet({
|
|
17
|
+
name: `pm.${this.name}`,
|
|
18
|
+
actions: ['orchestratorConfig:*'],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// --- Register Dynamic Tools ---
|
|
22
|
+
// Each configured sub-agent becomes a callable tool for its leader.
|
|
23
|
+
// Uses createReactAgent (LangGraph public API) instead of private AIEmployee class.
|
|
24
|
+
// Tools are registered via app.aiManager.toolsManager (public API from @nocobase/ai core).
|
|
25
|
+
const toolsManager = this.app.aiManager.toolsManager;
|
|
26
|
+
toolsManager.registerDynamicTools(createDelegateToolsProvider(this));
|
|
27
|
+
|
|
28
|
+
// --- Register Tracing Resource (Phase 5) ---
|
|
29
|
+
// Custom read-only resource for the Swarm Tracing admin page.
|
|
30
|
+
registerTracingResource(this);
|
|
31
|
+
|
|
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
|
+
|
|
37
|
+
async install() {
|
|
38
|
+
// No seed data needed on first install
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async afterEnable() {}
|
|
42
|
+
async afterDisable() {}
|
|
43
|
+
async remove() {}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default PluginAgentOrchestratorServer;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Plugin } from '@nocobase/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom resource for the Swarm Tracing admin UI (Phase 5).
|
|
5
|
+
* Queries the dedicated orchestratorLogs collection instead of
|
|
6
|
+
* filtering aiConversations by JSONB (P2 fix: DB-engine agnostic).
|
|
7
|
+
*/
|
|
8
|
+
export function registerTracingResource(plugin: Plugin) {
|
|
9
|
+
const app = plugin.app;
|
|
10
|
+
|
|
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
|
+
|
|
21
|
+
try {
|
|
22
|
+
const [rows, count] = await repo.findAndCount({
|
|
23
|
+
sort,
|
|
24
|
+
offset: (Number(page) - 1) * Number(pageSize),
|
|
25
|
+
limit: Number(pageSize),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
ctx.body = {
|
|
29
|
+
data: rows.map((row: any) => ({
|
|
30
|
+
id: row.id,
|
|
31
|
+
leaderUsername: row.leaderUsername,
|
|
32
|
+
subAgentUsername: row.subAgentUsername,
|
|
33
|
+
toolName: row.toolName,
|
|
34
|
+
task: row.task,
|
|
35
|
+
result: row.result,
|
|
36
|
+
status: row.status,
|
|
37
|
+
depth: row.depth,
|
|
38
|
+
durationMs: row.durationMs,
|
|
39
|
+
error: row.error,
|
|
40
|
+
userId: row.userId,
|
|
41
|
+
createdAt: row.createdAt,
|
|
42
|
+
})),
|
|
43
|
+
meta: {
|
|
44
|
+
count,
|
|
45
|
+
page: Number(page),
|
|
46
|
+
pageSize: Number(pageSize),
|
|
47
|
+
totalPage: Math.ceil(count / Number(pageSize)),
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
} catch (e) {
|
|
51
|
+
ctx.log.error('[AgentOrchestrator] Tracing list error', e);
|
|
52
|
+
ctx.body = { data: [], meta: { count: 0 } };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await next();
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get a single delegation log by ID.
|
|
60
|
+
*/
|
|
61
|
+
async get(ctx, next) {
|
|
62
|
+
const { filterByTk } = ctx.action.params;
|
|
63
|
+
|
|
64
|
+
if (!filterByTk) {
|
|
65
|
+
ctx.throw(400, 'id is required');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const repo = ctx.db.getRepository('orchestratorLogs');
|
|
71
|
+
const log = await repo.findOne({
|
|
72
|
+
filter: { id: filterByTk },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
ctx.body = { data: log };
|
|
76
|
+
} catch (e) {
|
|
77
|
+
ctx.log.error('[AgentOrchestrator] Tracing get error', e);
|
|
78
|
+
ctx.body = { data: null };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await next();
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ACL: allow admin access to tracing endpoints
|
|
87
|
+
app.acl.registerSnippet({
|
|
88
|
+
name: `pm.${plugin.name}.tracing`,
|
|
89
|
+
actions: ['orchestratorTracing:list', 'orchestratorTracing:get'],
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { Context } from '@nocobase/actions';
|
|
3
|
+
// @ts-ignore - subpath export types resolve at build time via NocoBase bundler
|
|
4
|
+
import { createReactAgent } from '@langchain/langgraph/prebuilt';
|
|
5
|
+
import { DynamicStructuredTool } from '@langchain/core/tools';
|
|
6
|
+
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
|
|
7
|
+
import type PluginAIServer from '@nocobase/plugin-ai/dist/server';
|
|
8
|
+
import type { ToolsEntry } from '@nocobase/ai';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Maximum delegation depth key stored in ctx metadata.
|
|
12
|
+
* Used to prevent circular/recursive delegation chains.
|
|
13
|
+
*/
|
|
14
|
+
const ORCHESTRATOR_DEPTH_KEY = '__orchestratorDepth';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates one dynamic tool per configured sub-agent for a given leader.
|
|
18
|
+
* Uses Strategy B (Per-SubAgent Tool): each sub-agent becomes a separate tool
|
|
19
|
+
* with its own name and description, making LLM tool selection natural.
|
|
20
|
+
*
|
|
21
|
+
* Architecture:
|
|
22
|
+
* - Uses createReactAgent (public LangGraph API) for agent execution
|
|
23
|
+
* - Uses plugin-ai's getLLMService() for LLM model resolution
|
|
24
|
+
* - Uses core app.aiManager.toolsManager.listTools() for tool resolution
|
|
25
|
+
* (same manager that AIEmployee uses — see ai-employee.ts:1286)
|
|
26
|
+
* - Depth enforcement via ctx metadata tracking
|
|
27
|
+
* - Per-leader scoping via invoke-time check (core ToolsOptions has no
|
|
28
|
+
* leaderUsername field, so scoping is enforced in the invoke callback)
|
|
29
|
+
*/
|
|
30
|
+
export function createDelegateToolsProvider(plugin: any) {
|
|
31
|
+
return async (register: any) => {
|
|
32
|
+
try {
|
|
33
|
+
const configRepo = plugin.db.getRepository('orchestratorConfig');
|
|
34
|
+
if (!configRepo) return;
|
|
35
|
+
|
|
36
|
+
const configs = await configRepo.find({
|
|
37
|
+
filter: { enabled: true },
|
|
38
|
+
});
|
|
39
|
+
if (!configs?.length) return;
|
|
40
|
+
|
|
41
|
+
// Build a lookup: which leaders are allowed to use each delegate tool
|
|
42
|
+
// Multiple leaders may share the same sub-agent, but each leader must
|
|
43
|
+
// have an explicit rule.
|
|
44
|
+
const leadersByTool = new Map<string, Set<string>>();
|
|
45
|
+
for (const config of configs) {
|
|
46
|
+
const toolName = `delegate_to_${config.subAgentUsername.replace(/[^a-zA-Z0-9_-]/g, '_')}`;
|
|
47
|
+
if (!leadersByTool.has(toolName)) {
|
|
48
|
+
leadersByTool.set(toolName, new Set());
|
|
49
|
+
}
|
|
50
|
+
leadersByTool.get(toolName)!.add(config.leaderUsername);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// De-duplicate: register one tool per sub-agent (not per config row)
|
|
54
|
+
const seenSubAgents = new Set<string>();
|
|
55
|
+
|
|
56
|
+
for (const config of configs) {
|
|
57
|
+
const { leaderUsername, subAgentUsername, maxDepth, timeout } = config;
|
|
58
|
+
|
|
59
|
+
// Skip if we already registered this sub-agent's tool
|
|
60
|
+
if (seenSubAgents.has(subAgentUsername)) continue;
|
|
61
|
+
seenSubAgents.add(subAgentUsername);
|
|
62
|
+
|
|
63
|
+
// Fetch the sub-agent employee model for its description and LLM config
|
|
64
|
+
const subAgentEmployee = await plugin.db.getRepository('aiEmployees').findOne({
|
|
65
|
+
filter: { username: subAgentUsername },
|
|
66
|
+
});
|
|
67
|
+
if (!subAgentEmployee) continue;
|
|
68
|
+
|
|
69
|
+
const toolName = `delegate_to_${subAgentUsername.replace(/[^a-zA-Z0-9_-]/g, '_')}`;
|
|
70
|
+
const toolDescription = [
|
|
71
|
+
`Delegate a task to the AI Employee "${subAgentEmployee.nickname || subAgentUsername}".`,
|
|
72
|
+
subAgentEmployee.about ? `Specialist profile: ${subAgentEmployee.about.substring(0, 200)}` : '',
|
|
73
|
+
'The sub-agent will execute the task independently and return its final answer.',
|
|
74
|
+
].filter(Boolean).join(' ');
|
|
75
|
+
|
|
76
|
+
// Capture the allowed leaders for this tool (for invoke-time scoping)
|
|
77
|
+
const allowedLeaders = leadersByTool.get(toolName)!;
|
|
78
|
+
|
|
79
|
+
register.registerTools({
|
|
80
|
+
scope: 'CUSTOM',
|
|
81
|
+
execution: 'backend',
|
|
82
|
+
defaultPermission: 'ALLOW',
|
|
83
|
+
silence: false,
|
|
84
|
+
introduction: {
|
|
85
|
+
title: `[Sub-Agent] ${subAgentEmployee.nickname || subAgentUsername}`,
|
|
86
|
+
about: toolDescription,
|
|
87
|
+
},
|
|
88
|
+
definition: {
|
|
89
|
+
name: toolName,
|
|
90
|
+
description: toolDescription,
|
|
91
|
+
schema: z.object({
|
|
92
|
+
task: z.string().describe('The detailed task description for the sub-agent to execute.'),
|
|
93
|
+
context: z.string().optional().describe('Optional additional context to help the sub-agent understand the task better.'),
|
|
94
|
+
}),
|
|
95
|
+
},
|
|
96
|
+
invoke: async (ctx: Context, args: { task: string; context?: string }, id: string) => {
|
|
97
|
+
// --- P2 FIX: Per-leader scoping at invoke time ---
|
|
98
|
+
// Core ToolsOptions doesn't support leaderUsername field, so
|
|
99
|
+
// we enforce scoping here by checking the calling employee.
|
|
100
|
+
const callingEmployee = (ctx as any)._currentAIEmployee?.username
|
|
101
|
+
|| (ctx as any).state?.currentAIEmployee;
|
|
102
|
+
if (callingEmployee && !allowedLeaders.has(callingEmployee)) {
|
|
103
|
+
return {
|
|
104
|
+
status: 'error' as const,
|
|
105
|
+
content: `Employee "${callingEmployee}" is not authorized to delegate to "${subAgentUsername}". Configure an orchestration rule first.`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return invokeDelegateTask(ctx, plugin, {
|
|
110
|
+
leaderUsername: callingEmployee || Array.from(allowedLeaders)[0] || '',
|
|
111
|
+
subAgentUsername,
|
|
112
|
+
subAgentEmployee,
|
|
113
|
+
task: args.task,
|
|
114
|
+
context: args.context,
|
|
115
|
+
maxDepth: maxDepth ?? 1,
|
|
116
|
+
timeout: timeout ?? 120000,
|
|
117
|
+
toolCallId: id,
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
} catch (e) {
|
|
123
|
+
plugin.app.log.error('[AgentOrchestrator] Failed to register delegate tools', e);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Core execution logic using createReactAgent (public LangGraph API).
|
|
130
|
+
*
|
|
131
|
+
* This approach mirrors plugin-sub-agent's proven pattern:
|
|
132
|
+
* 1. Get LLM model via aiPlugin.aiManager.getLLMService()
|
|
133
|
+
* 2. Resolve sub-agent's tools via core app.aiManager.toolsManager.listTools()
|
|
134
|
+
* 3. Build a standalone createReactAgent with the model + tools
|
|
135
|
+
* 4. Stream results and extract final AI message
|
|
136
|
+
*
|
|
137
|
+
* Tool resolution uses the CORE toolsManager (app.aiManager.toolsManager) —
|
|
138
|
+
* the same manager that AIEmployee.getToolsMap() uses (see ai-employee.ts:1286).
|
|
139
|
+
* This ensures tool names in skillSettings.skills[].name match correctly.
|
|
140
|
+
*
|
|
141
|
+
* skillSettings.skills shape (verified against ai-employee.ts:1028):
|
|
142
|
+
* { name: string, autoCall: boolean }[]
|
|
143
|
+
*/
|
|
144
|
+
async function invokeDelegateTask(
|
|
145
|
+
ctx: Context,
|
|
146
|
+
plugin: any,
|
|
147
|
+
options: {
|
|
148
|
+
leaderUsername: string;
|
|
149
|
+
subAgentUsername: string;
|
|
150
|
+
subAgentEmployee: any;
|
|
151
|
+
task: string;
|
|
152
|
+
context?: string;
|
|
153
|
+
maxDepth: number;
|
|
154
|
+
timeout: number;
|
|
155
|
+
toolCallId: string;
|
|
156
|
+
},
|
|
157
|
+
) {
|
|
158
|
+
const { leaderUsername, subAgentUsername, subAgentEmployee, task, context, maxDepth, timeout, toolCallId } = options;
|
|
159
|
+
|
|
160
|
+
// --- P1: Depth enforcement ---
|
|
161
|
+
const currentDepth: number = (ctx as any)[ORCHESTRATOR_DEPTH_KEY] ?? 0;
|
|
162
|
+
if (currentDepth >= maxDepth) {
|
|
163
|
+
return {
|
|
164
|
+
status: 'error' as const,
|
|
165
|
+
content: `Delegation depth limit reached (${currentDepth}/${maxDepth}). Sub-agent "${subAgentUsername}" cannot delegate further.`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const startTime = Date.now();
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const aiPlugin = ctx.app.pm.get('ai') as PluginAIServer;
|
|
173
|
+
if (!aiPlugin) {
|
|
174
|
+
throw new Error('Plugin AI is not installed or enabled');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// --- Step 1: Resolve LLM model from sub-agent's employee config ---
|
|
178
|
+
const modelSettings = subAgentEmployee.modelSettings;
|
|
179
|
+
if (!modelSettings?.llmService || !modelSettings?.model) {
|
|
180
|
+
throw new Error(`Sub-agent "${subAgentUsername}" has no LLM model configured. Please configure a model in the AI Employee settings.`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const { provider } = await aiPlugin.aiManager.getLLMService({
|
|
184
|
+
llmService: modelSettings.llmService,
|
|
185
|
+
model: modelSettings.model,
|
|
186
|
+
});
|
|
187
|
+
const chatModel = provider.createModel();
|
|
188
|
+
|
|
189
|
+
// --- Step 2: Resolve tools via CORE toolsManager ---
|
|
190
|
+
// Uses app.aiManager.toolsManager (same as AIEmployee.getToolsMap at ai-employee.ts:1286)
|
|
191
|
+
// NOT plugin-ai's local toolManager (which has a different grouped format).
|
|
192
|
+
const coreToolsManager = ctx.app.aiManager.toolsManager;
|
|
193
|
+
const allTools: ToolsEntry[] = await coreToolsManager.listTools();
|
|
194
|
+
|
|
195
|
+
// skillSettings.skills is { name: string, autoCall: boolean }[]
|
|
196
|
+
// (verified at ai-employee.ts:1028-1029)
|
|
197
|
+
const employeeSkills: string[] = (subAgentEmployee.skillSettings?.skills ?? [])
|
|
198
|
+
.map((s: any) => (typeof s === 'string' ? s : s.name))
|
|
199
|
+
.filter(Boolean);
|
|
200
|
+
|
|
201
|
+
const langchainTools: DynamicStructuredTool[] = [];
|
|
202
|
+
|
|
203
|
+
for (const toolEntry of allTools) {
|
|
204
|
+
const toolName = toolEntry.definition.name;
|
|
205
|
+
if (!toolName) continue;
|
|
206
|
+
|
|
207
|
+
// Only include tools that the sub-agent employee is configured to use.
|
|
208
|
+
// Also skip our own delegate_to_* tools to prevent circular delegation
|
|
209
|
+
// (belt-and-suspenders with the depth check above).
|
|
210
|
+
if (!employeeSkills.includes(toolName) || toolName.startsWith('delegate_to_')) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
langchainTools.push(
|
|
215
|
+
new DynamicStructuredTool({
|
|
216
|
+
name: toolName.replace(/[^a-zA-Z0-9_-]/g, '_'),
|
|
217
|
+
description: toolEntry.definition.description || toolName,
|
|
218
|
+
schema: (toolEntry.definition.schema || z.object({})) as any,
|
|
219
|
+
func: async (toolArgs) => {
|
|
220
|
+
// Forward the invoke with depth tracking
|
|
221
|
+
const invokeCtx = Object.create(ctx);
|
|
222
|
+
(invokeCtx as any)[ORCHESTRATOR_DEPTH_KEY] = currentDepth + 1;
|
|
223
|
+
|
|
224
|
+
const res = await toolEntry.invoke(invokeCtx, toolArgs, `orch-${toolCallId}`);
|
|
225
|
+
if (res?.status === 'error') {
|
|
226
|
+
throw new Error(`Tool <${toolName}> failed: ${res.content}`);
|
|
227
|
+
}
|
|
228
|
+
return typeof res?.content === 'string' ? res.content : JSON.stringify(res);
|
|
229
|
+
},
|
|
230
|
+
}),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// --- Step 3: Build the agent ---
|
|
235
|
+
const abortController = new AbortController();
|
|
236
|
+
const executor = createReactAgent({
|
|
237
|
+
llm: chatModel,
|
|
238
|
+
tools: langchainTools,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// --- Step 4: Construct messages ---
|
|
242
|
+
const systemPrompt = subAgentEmployee.chatSettings?.systemPrompt
|
|
243
|
+
|| subAgentEmployee.bio
|
|
244
|
+
|| `You are an AI assistant named "${subAgentEmployee.nickname || subAgentUsername}". ${subAgentEmployee.about || ''}`;
|
|
245
|
+
|
|
246
|
+
const combinedTask = context
|
|
247
|
+
? `Task: ${task}\n\nContext Provided:\n${context}`
|
|
248
|
+
: `Task: ${task}`;
|
|
249
|
+
|
|
250
|
+
// --- Step 5: Execute with timeout + abort ---
|
|
251
|
+
// P3 FIX: AbortController signal cancels the in-flight stream on timeout,
|
|
252
|
+
// preventing continued token consumption after the timeout fires.
|
|
253
|
+
const invokePromise = executeAgent(executor, systemPrompt, combinedTask, abortController.signal);
|
|
254
|
+
|
|
255
|
+
const result = await Promise.race([
|
|
256
|
+
invokePromise,
|
|
257
|
+
createTimeout(timeout, subAgentUsername, abortController),
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
const content = (result as string) || 'Sub-agent completed the task but produced no output.';
|
|
261
|
+
|
|
262
|
+
// Log successful execution for tracing
|
|
263
|
+
await logDelegation(ctx, plugin, {
|
|
264
|
+
leaderUsername,
|
|
265
|
+
subAgentUsername,
|
|
266
|
+
task,
|
|
267
|
+
result: content,
|
|
268
|
+
status: 'success',
|
|
269
|
+
depth: currentDepth,
|
|
270
|
+
durationMs: Date.now() - startTime,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
status: 'success' as const,
|
|
275
|
+
content,
|
|
276
|
+
};
|
|
277
|
+
} catch (e) {
|
|
278
|
+
plugin.app.log.error(`[AgentOrchestrator] Sub-agent ${subAgentUsername} failed`, e);
|
|
279
|
+
|
|
280
|
+
// Log failed execution for tracing
|
|
281
|
+
await logDelegation(ctx, plugin, {
|
|
282
|
+
leaderUsername,
|
|
283
|
+
subAgentUsername,
|
|
284
|
+
task,
|
|
285
|
+
result: '',
|
|
286
|
+
status: 'error',
|
|
287
|
+
depth: currentDepth,
|
|
288
|
+
durationMs: Date.now() - startTime,
|
|
289
|
+
error: e.message,
|
|
290
|
+
}).catch(() => {}); // Don't let logging errors mask the real error
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
status: 'error' as const,
|
|
294
|
+
content: `Sub-agent "${subAgentUsername}" failed: ${e.message}`,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Log a delegation event to the orchestratorLogs collection for observability.
|
|
301
|
+
*/
|
|
302
|
+
async function logDelegation(
|
|
303
|
+
ctx: Context,
|
|
304
|
+
plugin: any,
|
|
305
|
+
data: {
|
|
306
|
+
leaderUsername: string;
|
|
307
|
+
subAgentUsername: string;
|
|
308
|
+
task: string;
|
|
309
|
+
result: string;
|
|
310
|
+
status: string;
|
|
311
|
+
depth: number;
|
|
312
|
+
durationMs: number;
|
|
313
|
+
error?: string;
|
|
314
|
+
},
|
|
315
|
+
) {
|
|
316
|
+
try {
|
|
317
|
+
const logsRepo = plugin.db.getRepository('orchestratorLogs');
|
|
318
|
+
if (!logsRepo) return;
|
|
319
|
+
|
|
320
|
+
await logsRepo.create({
|
|
321
|
+
values: {
|
|
322
|
+
leaderUsername: data.leaderUsername,
|
|
323
|
+
subAgentUsername: data.subAgentUsername,
|
|
324
|
+
toolName: `delegate_to_${data.subAgentUsername}`,
|
|
325
|
+
task: data.task.substring(0, 2000),
|
|
326
|
+
result: data.result.substring(0, 5000),
|
|
327
|
+
status: data.status,
|
|
328
|
+
depth: data.depth,
|
|
329
|
+
durationMs: data.durationMs,
|
|
330
|
+
error: data.error?.substring(0, 2000),
|
|
331
|
+
userId: ctx.auth?.user?.id || ctx.state?.currentUser?.id,
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
} catch (e) {
|
|
335
|
+
plugin.app.log.warn('[AgentOrchestrator] Failed to log delegation event', e);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Execute the agent and extract the final AI message content.
|
|
341
|
+
* Uses stream mode to collect all chunks, similar to plugin-sub-agent.
|
|
342
|
+
* Accepts an AbortSignal so the stream can be cancelled on timeout.
|
|
343
|
+
*/
|
|
344
|
+
async function executeAgent(
|
|
345
|
+
executor: any,
|
|
346
|
+
systemPrompt: string,
|
|
347
|
+
task: string,
|
|
348
|
+
signal?: AbortSignal,
|
|
349
|
+
): Promise<string> {
|
|
350
|
+
const streamOptions: any = { recursionLimit: 50, streamMode: 'messages' };
|
|
351
|
+
if (signal) {
|
|
352
|
+
streamOptions.signal = signal;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const stream = await executor.stream(
|
|
356
|
+
{
|
|
357
|
+
messages: [new SystemMessage(systemPrompt), new HumanMessage(task)],
|
|
358
|
+
},
|
|
359
|
+
streamOptions,
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
let aiContentCache = '';
|
|
363
|
+
|
|
364
|
+
for await (const chunk of stream) {
|
|
365
|
+
// Check abort between chunks
|
|
366
|
+
if (signal?.aborted) break;
|
|
367
|
+
|
|
368
|
+
const [message] = chunk;
|
|
369
|
+
if (message.getType() === 'ai' && message.content) {
|
|
370
|
+
aiContentCache += message.content.toString();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return aiContentCache || '';
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Create a timeout promise that rejects after the specified duration.
|
|
379
|
+
* P3 FIX: Also triggers AbortController to cancel the in-flight stream.
|
|
380
|
+
*/
|
|
381
|
+
function createTimeout(ms: number, agentName: string, abortController?: AbortController): Promise<never> {
|
|
382
|
+
return new Promise((_, reject) =>
|
|
383
|
+
setTimeout(() => {
|
|
384
|
+
abortController?.abort();
|
|
385
|
+
reject(new Error(`Sub-agent "${agentName}" timed out after ${ms / 1000}s`));
|
|
386
|
+
}, ms),
|
|
387
|
+
);
|
|
388
|
+
}
|