plugin-agent-orchestrator 1.0.20 → 1.0.21
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/dist/client/hooks/useRunEventStream.d.ts +22 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -1
- package/dist/externalVersion.js +6 -6
- package/dist/server/collections/agent-execution-spans.js +24 -0
- package/dist/server/collections/agent-loop-runs.js +36 -0
- package/dist/server/collections/orchestrator-config.js +14 -0
- package/dist/server/migrations/20260601000000-add-token-fields.d.ts +7 -0
- package/dist/server/migrations/20260601000000-add-token-fields.js +101 -0
- package/dist/server/plugin.js +47 -0
- package/dist/server/resources/agent-loop.js +33 -25
- package/dist/server/resources/tracing.js +5 -8
- package/dist/server/services/AgentHarness.d.ts +2 -0
- package/dist/server/services/AgentHarness.js +56 -90
- package/dist/server/services/AgentLoopController.d.ts +33 -20
- package/dist/server/services/AgentLoopController.js +164 -125
- package/dist/server/services/AgentLoopRepository.js +16 -34
- package/dist/server/services/AgentLoopService.d.ts +28 -18
- package/dist/server/services/AgentLoopService.js +7 -1
- package/dist/server/services/AgentPlannerService.js +5 -25
- package/dist/server/services/AgentRegistryService.d.ts +8 -0
- package/dist/server/services/AgentRegistryService.js +34 -24
- package/dist/server/services/CircuitBreaker.d.ts +40 -0
- package/dist/server/services/CircuitBreaker.js +120 -0
- package/dist/server/services/ContextAggregator.d.ts +45 -0
- package/dist/server/services/ContextAggregator.js +201 -0
- package/dist/server/services/ExecutionSpanService.js +2 -5
- package/dist/server/services/RunEventBus.d.ts +9 -0
- package/dist/server/services/RunEventBus.js +73 -0
- package/dist/server/services/TokenTracker.d.ts +62 -0
- package/dist/server/services/TokenTracker.js +173 -0
- package/dist/server/tools/agent-loop.d.ts +8 -8
- package/dist/server/tools/agent-loop.js +30 -63
- package/dist/server/tools/delegate-task.js +14 -72
- package/dist/server/tools/orchestrator-plan.d.ts +6 -6
- package/dist/server/tools/orchestrator-plan.js +10 -47
- package/dist/server/types.d.ts +47 -0
- package/dist/server/types.js +24 -0
- package/dist/server/utils/ctx-utils.d.ts +30 -0
- package/dist/server/utils/ctx-utils.js +152 -0
- package/dist/server/utils/logging.d.ts +6 -0
- package/dist/server/utils/logging.js +86 -0
- package/package.json +44 -44
- package/src/client/AgentRunsTab.tsx +764 -764
- package/src/client/HarnessProfilesTab.tsx +247 -247
- package/src/client/OrchestratorSettings.tsx +106 -106
- package/src/client/RulesTab.tsx +716 -716
- package/src/client/hooks/useRunEventStream.ts +76 -0
- package/src/client/index.tsx +2 -1
- package/src/client/plugin.tsx +27 -27
- package/src/client/skill-hub/components/LoopSettings.tsx +331 -331
- package/src/client/skill-hub/index.tsx +51 -51
- package/src/client/skill-hub/tools/InteractionSchemasProvider.tsx +99 -99
- package/src/client/skill-hub/tools/SkillHubCard.tsx +109 -109
- package/src/client/skill-hub/tools/loopTemplates.ts +52 -52
- package/src/client/skill-hub/tools/registerSkillLoopCards.ts +58 -58
- package/src/client/tools/PlanApprovalCard.tsx +175 -175
- package/src/client/tools/registerOrchestratorCards.ts +7 -7
- package/src/server/__tests__/agent-loop-controller.test.ts +375 -0
- package/src/server/__tests__/circuit-breaker.test.ts +169 -0
- package/src/server/__tests__/context-aggregator.test.ts +222 -0
- package/src/server/__tests__/parallel-execution.test.ts +318 -0
- package/src/server/__tests__/smoke.test.ts +120 -0
- package/src/server/collections/agent-execution-spans.ts +24 -0
- package/src/server/collections/agent-harness-profiles.ts +59 -59
- package/src/server/collections/agent-loop-events.ts +71 -71
- package/src/server/collections/agent-loop-runs.ts +38 -1
- package/src/server/collections/agent-loop-steps.ts +144 -144
- package/src/server/collections/orchestrator-config.ts +14 -0
- package/src/server/collections/skill-executions.ts +106 -106
- package/src/server/collections/skill-loop-configs.ts +65 -65
- package/src/server/migrations/20260524000000-add-agent-loop-fields-to-skill-executions.ts +30 -30
- package/src/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.ts +142 -142
- package/src/server/migrations/20260601000000-add-token-fields.ts +89 -0
- package/src/server/plugin.ts +53 -0
- package/src/server/resources/agent-loop.ts +21 -12
- package/src/server/resources/tracing.ts +3 -7
- package/src/server/services/AgentHarness.ts +78 -116
- package/src/server/services/AgentLoopController.ts +197 -122
- package/src/server/services/AgentLoopRepository.ts +9 -25
- package/src/server/services/AgentLoopService.ts +13 -1
- package/src/server/services/AgentPlanValidator.ts +73 -73
- package/src/server/services/AgentPlannerService.ts +2 -25
- package/src/server/services/AgentRegistryService.ts +40 -31
- package/src/server/services/CircuitBreaker.ts +116 -0
- package/src/server/services/ContextAggregator.ts +239 -0
- package/src/server/services/ExecutionSpanService.ts +2 -4
- package/src/server/services/RunEventBus.ts +45 -0
- package/src/server/services/TokenTracker.ts +209 -0
- package/src/server/skill-hub/plugin.ts +898 -898
- package/src/server/skill-hub/tasks/SkillExecutionTask.ts +460 -460
- package/src/server/tools/agent-loop.ts +18 -57
- package/src/server/tools/delegate-task.ts +11 -93
- package/src/server/tools/orchestrator-plan.ts +26 -50
- package/src/server/tools/skill-execute.ts +160 -160
- package/src/server/types.ts +55 -0
- package/src/server/utils/ctx-utils.ts +118 -0
- package/src/server/utils/logging.ts +63 -0
package/src/client/RulesTab.tsx
CHANGED
|
@@ -1,716 +1,716 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
Table,
|
|
4
|
-
Button,
|
|
5
|
-
Drawer,
|
|
6
|
-
Form,
|
|
7
|
-
InputNumber,
|
|
8
|
-
Switch,
|
|
9
|
-
Space,
|
|
10
|
-
Popconfirm,
|
|
11
|
-
Card,
|
|
12
|
-
message,
|
|
13
|
-
Tag,
|
|
14
|
-
Typography,
|
|
15
|
-
Alert,
|
|
16
|
-
Collapse,
|
|
17
|
-
Empty,
|
|
18
|
-
Select,
|
|
19
|
-
} from 'antd';
|
|
20
|
-
import {
|
|
21
|
-
PlusOutlined,
|
|
22
|
-
EditOutlined,
|
|
23
|
-
DeleteOutlined,
|
|
24
|
-
SwapRightOutlined,
|
|
25
|
-
WarningOutlined,
|
|
26
|
-
ThunderboltOutlined,
|
|
27
|
-
} from '@ant-design/icons';
|
|
28
|
-
import { useAPIClient, useRequest } from '@nocobase/client';
|
|
29
|
-
import { AIEmployeeSelect } from './AIEmployeeSelect';
|
|
30
|
-
import { useAIEmployees } from './AIEmployeesContext';
|
|
31
|
-
|
|
32
|
-
const { Text } = Typography;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Mirrors server-side `sanitizeToolPart` in delegate-task.ts so we can compute
|
|
36
|
-
* the expected delegation tool names here and detect when the leader hasn't
|
|
37
|
-
* added them to its skillSettings.
|
|
38
|
-
*/
|
|
39
|
-
const sanitizeToolPart = (value: string) => (value || '').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
40
|
-
const expectedDelegateToolName = (leader: string, sub: string) =>
|
|
41
|
-
`delegate_${sanitizeToolPart(leader)}_to_${sanitizeToolPart(sub)}`;
|
|
42
|
-
const expectedDispatchToolName = (leader: string) => `dispatch_subagents_${sanitizeToolPart(leader)}`;
|
|
43
|
-
const controllerToolNames = [
|
|
44
|
-
'orchestrator_plan_goal',
|
|
45
|
-
'orchestrator_execute_plan',
|
|
46
|
-
'orchestrator_status',
|
|
47
|
-
'orchestrator_cancel',
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
export const RulesTab: React.FC = () => {
|
|
51
|
-
const api = useAPIClient();
|
|
52
|
-
const [visible, setVisible] = useState(false);
|
|
53
|
-
const [editingRecord, setEditingRecord] = useState<any>(null);
|
|
54
|
-
const [form] = Form.useForm();
|
|
55
|
-
|
|
56
|
-
const { data, loading, refresh } = useRequest({
|
|
57
|
-
url: 'orchestratorConfig:list',
|
|
58
|
-
params: {
|
|
59
|
-
sort: ['-createdAt'],
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
const { data: llmServicesData, loading: llmLoading } = useRequest({
|
|
64
|
-
url: 'ai:listAllEnabledModels',
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const { data: harnessProfilesData, loading: harnessLoading } = useRequest({
|
|
68
|
-
url: 'agentHarnessProfiles:list',
|
|
69
|
-
params: {
|
|
70
|
-
filter: { enabled: true },
|
|
71
|
-
sort: ['tag'],
|
|
72
|
-
pageSize: 100,
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const llmServices = React.useMemo(() => {
|
|
77
|
-
const raw = (llmServicesData as any)?.data ?? llmServicesData;
|
|
78
|
-
if (Array.isArray(raw)) return raw;
|
|
79
|
-
return Array.isArray(raw?.data) ? raw.data : [];
|
|
80
|
-
}, [llmServicesData]);
|
|
81
|
-
|
|
82
|
-
const harnessProfiles = React.useMemo(() => {
|
|
83
|
-
const raw = (harnessProfilesData as any)?.data ?? harnessProfilesData;
|
|
84
|
-
if (Array.isArray(raw)) return raw;
|
|
85
|
-
return Array.isArray(raw?.data) ? raw.data : [];
|
|
86
|
-
}, [harnessProfilesData]);
|
|
87
|
-
|
|
88
|
-
// P3 FIX: Use shared context instead of duplicate API call
|
|
89
|
-
const { employeeMap, skillsMap, refresh: refreshEmployees } = useAIEmployees();
|
|
90
|
-
const rules = React.useMemo(() => {
|
|
91
|
-
const rows = (data as any)?.data;
|
|
92
|
-
return Array.isArray(rows) ? rows : [];
|
|
93
|
-
}, [data]);
|
|
94
|
-
|
|
95
|
-
const handleAddSkillsToEmployee = async (employeeUsername: string, toolNames: string[]) => {
|
|
96
|
-
try {
|
|
97
|
-
// Re-fetch the leader to merge its current skills (skillsMap may be stale).
|
|
98
|
-
const leaderResp = await api.request({
|
|
99
|
-
url: 'aiEmployees:get',
|
|
100
|
-
params: { filterByTk: employeeUsername },
|
|
101
|
-
});
|
|
102
|
-
const leader = (leaderResp as any)?.data?.data;
|
|
103
|
-
if (!leader) {
|
|
104
|
-
message.error('Could not load AI employee.');
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
const existing = Array.isArray(leader.skillSettings?.skills) ? leader.skillSettings.skills : [];
|
|
108
|
-
const existingNames = new Set(existing.map((s: any) => (typeof s === 'string' ? s : s?.name)));
|
|
109
|
-
const missing = toolNames.filter((toolName) => !existingNames.has(toolName));
|
|
110
|
-
if (!missing.length) {
|
|
111
|
-
message.info('Skills already present.');
|
|
112
|
-
await refreshEmployees();
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
const nextSkills = [...existing, ...missing.map((name) => ({ name, autoCall: false }))];
|
|
116
|
-
await api.request({
|
|
117
|
-
url: 'aiEmployees:update',
|
|
118
|
-
method: 'put',
|
|
119
|
-
params: { filterByTk: employeeUsername },
|
|
120
|
-
data: { skillSettings: { ...(leader.skillSettings || {}), skills: nextSkills } },
|
|
121
|
-
});
|
|
122
|
-
message.success(`Added ${missing.length} skill${missing.length > 1 ? 's' : ''} to ${employeeUsername}.`);
|
|
123
|
-
await refreshEmployees();
|
|
124
|
-
} catch (e: any) {
|
|
125
|
-
message.error(`Auto-assign failed: ${e?.message || 'unknown error'}`);
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
const handleAddSkillToEmployee = async (employeeUsername: string, toolName: string) => {
|
|
130
|
-
await handleAddSkillsToEmployee(employeeUsername, [toolName]);
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const handleAutoAssignSkill = async (record: any) => {
|
|
134
|
-
await handleAddSkillToEmployee(
|
|
135
|
-
record.leaderUsername,
|
|
136
|
-
expectedDelegateToolName(record.leaderUsername, record.subAgentUsername),
|
|
137
|
-
);
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
const handleAutoAssignDispatchSkill = async (leaderUsername: string) => {
|
|
141
|
-
await handleAddSkillToEmployee(leaderUsername, expectedDispatchToolName(leaderUsername));
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const subAgentLeaderCount = React.useMemo(() => {
|
|
145
|
-
const counts = new Map<string, Set<string>>();
|
|
146
|
-
for (const rule of rules) {
|
|
147
|
-
const set = counts.get(rule.subAgentUsername) || new Set<string>();
|
|
148
|
-
set.add(rule.leaderUsername);
|
|
149
|
-
counts.set(rule.subAgentUsername, set);
|
|
150
|
-
}
|
|
151
|
-
return counts;
|
|
152
|
-
}, [rules]);
|
|
153
|
-
|
|
154
|
-
const aliasConflicts = React.useMemo(() => {
|
|
155
|
-
return Array.from(subAgentLeaderCount.entries())
|
|
156
|
-
.filter(([, leaders]) => leaders.size > 1)
|
|
157
|
-
.map(([sub, leaders]) => ({ sub, leaders: Array.from(leaders) }));
|
|
158
|
-
}, [subAgentLeaderCount]);
|
|
159
|
-
|
|
160
|
-
const groupedRules = React.useMemo(() => {
|
|
161
|
-
const groups = new Map<string, any[]>();
|
|
162
|
-
for (const rule of rules) {
|
|
163
|
-
const key = rule.leaderUsername || 'unknown';
|
|
164
|
-
let items = groups.get(key);
|
|
165
|
-
if (!items) {
|
|
166
|
-
items = [];
|
|
167
|
-
groups.set(key, items);
|
|
168
|
-
}
|
|
169
|
-
items.push(rule);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return Array.from(groups.entries()).map(([leaderUsername, items]) => ({
|
|
173
|
-
leaderUsername,
|
|
174
|
-
items,
|
|
175
|
-
}));
|
|
176
|
-
}, [rules]);
|
|
177
|
-
|
|
178
|
-
const handleOpen = (record?: any) => {
|
|
179
|
-
setEditingRecord(record);
|
|
180
|
-
if (record) {
|
|
181
|
-
form.setFieldsValue(record);
|
|
182
|
-
} else {
|
|
183
|
-
form.resetFields();
|
|
184
|
-
form.setFieldsValue({ enabled: true, maxDepth: 1, timeout: 120000, recursionLimit: 50, harnessTag: 'default' });
|
|
185
|
-
}
|
|
186
|
-
setVisible(true);
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
const handleClose = () => {
|
|
190
|
-
setVisible(false);
|
|
191
|
-
setEditingRecord(null);
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
const handleSave = async (values: any) => {
|
|
195
|
-
// Validate: leader !== subAgent
|
|
196
|
-
if (values.leaderUsername === values.subAgentUsername) {
|
|
197
|
-
message.error('Leader and Sub-Agent cannot be the same employee.');
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
try {
|
|
202
|
-
if (editingRecord) {
|
|
203
|
-
await api.request({
|
|
204
|
-
url: 'orchestratorConfig:update',
|
|
205
|
-
method: 'put',
|
|
206
|
-
params: { filterByTk: editingRecord.id },
|
|
207
|
-
data: values,
|
|
208
|
-
});
|
|
209
|
-
message.success('Rule updated');
|
|
210
|
-
} else {
|
|
211
|
-
await api.request({
|
|
212
|
-
url: 'orchestratorConfig:create',
|
|
213
|
-
method: 'post',
|
|
214
|
-
data: values,
|
|
215
|
-
});
|
|
216
|
-
message.success('Rule created');
|
|
217
|
-
}
|
|
218
|
-
handleClose();
|
|
219
|
-
refresh();
|
|
220
|
-
} catch (e: any) {
|
|
221
|
-
const msg = e?.response?.data?.errors?.[0]?.message || e.message;
|
|
222
|
-
message.error(`Save failed: ${msg}`);
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
const handleDelete = async (id: string) => {
|
|
227
|
-
try {
|
|
228
|
-
await api.request({
|
|
229
|
-
url: 'orchestratorConfig:destroy',
|
|
230
|
-
method: 'delete',
|
|
231
|
-
params: { filterByTk: id },
|
|
232
|
-
});
|
|
233
|
-
message.success('Rule deleted');
|
|
234
|
-
refresh();
|
|
235
|
-
} catch (e: any) {
|
|
236
|
-
message.error(`Delete failed: ${e.message}`);
|
|
237
|
-
}
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
const columns = [
|
|
241
|
-
{
|
|
242
|
-
title: 'Leader (Orchestrator)',
|
|
243
|
-
dataIndex: 'leaderUsername',
|
|
244
|
-
key: 'leaderUsername',
|
|
245
|
-
render: (username: string) => <Tag color="blue">{employeeMap.get(username) || username}</Tag>,
|
|
246
|
-
},
|
|
247
|
-
{
|
|
248
|
-
title: '',
|
|
249
|
-
key: 'arrow',
|
|
250
|
-
width: 50,
|
|
251
|
-
render: () => <SwapRightOutlined style={{ color: '#999', fontSize: 18 }} />,
|
|
252
|
-
},
|
|
253
|
-
{
|
|
254
|
-
title: 'Sub-Agent',
|
|
255
|
-
dataIndex: 'subAgentUsername',
|
|
256
|
-
key: 'subAgentUsername',
|
|
257
|
-
render: (username: string) => <Tag color="green">{employeeMap.get(username) || username}</Tag>,
|
|
258
|
-
},
|
|
259
|
-
{
|
|
260
|
-
title: 'Harness',
|
|
261
|
-
dataIndex: 'harnessTag',
|
|
262
|
-
key: 'harnessTag',
|
|
263
|
-
width: 120,
|
|
264
|
-
render: (tag: string) => <Tag color="purple">{tag || 'default'}</Tag>,
|
|
265
|
-
},
|
|
266
|
-
{
|
|
267
|
-
title: 'Max Depth',
|
|
268
|
-
dataIndex: 'maxDepth',
|
|
269
|
-
key: 'maxDepth',
|
|
270
|
-
width: 100,
|
|
271
|
-
render: (v: number) => v ?? 1,
|
|
272
|
-
},
|
|
273
|
-
{
|
|
274
|
-
title: 'Timeout',
|
|
275
|
-
dataIndex: 'timeout',
|
|
276
|
-
key: 'timeout',
|
|
277
|
-
width: 100,
|
|
278
|
-
render: (v: number) => `${((v ?? 120000) / 1000).toFixed(0)}s`,
|
|
279
|
-
},
|
|
280
|
-
{
|
|
281
|
-
title: 'LLM Override',
|
|
282
|
-
key: 'llmOverride',
|
|
283
|
-
width: 140,
|
|
284
|
-
render: (_: any, record: any) => {
|
|
285
|
-
if (record.llmService && record.model) {
|
|
286
|
-
const svc = llmServices.find((s: any) => s.llmService === record.llmService);
|
|
287
|
-
const svcName = svc ? svc.llmServiceTitle : record.llmService;
|
|
288
|
-
return (
|
|
289
|
-
<Space direction="vertical" size={0}>
|
|
290
|
-
<Text style={{ fontSize: 12 }}>{svcName}</Text>
|
|
291
|
-
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
292
|
-
{record.model}
|
|
293
|
-
</Text>
|
|
294
|
-
</Space>
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
return (
|
|
298
|
-
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
299
|
-
Inherited
|
|
300
|
-
</Text>
|
|
301
|
-
);
|
|
302
|
-
},
|
|
303
|
-
},
|
|
304
|
-
{
|
|
305
|
-
title: 'Enabled',
|
|
306
|
-
dataIndex: 'enabled',
|
|
307
|
-
key: 'enabled',
|
|
308
|
-
width: 80,
|
|
309
|
-
render: (enabled: boolean, record: any) => (
|
|
310
|
-
<Switch
|
|
311
|
-
checked={enabled}
|
|
312
|
-
size="small"
|
|
313
|
-
onChange={async (checked) => {
|
|
314
|
-
await api.request({
|
|
315
|
-
url: 'orchestratorConfig:update',
|
|
316
|
-
method: 'put',
|
|
317
|
-
params: { filterByTk: record.id },
|
|
318
|
-
data: { enabled: checked },
|
|
319
|
-
});
|
|
320
|
-
refresh();
|
|
321
|
-
}}
|
|
322
|
-
/>
|
|
323
|
-
),
|
|
324
|
-
},
|
|
325
|
-
{
|
|
326
|
-
title: 'Skill',
|
|
327
|
-
key: 'skill',
|
|
328
|
-
width: 150,
|
|
329
|
-
render: (_: any, record: any) => {
|
|
330
|
-
const expected = expectedDelegateToolName(record.leaderUsername, record.subAgentUsername);
|
|
331
|
-
const leaderSkills = skillsMap.get(record.leaderUsername);
|
|
332
|
-
if (!leaderSkills) {
|
|
333
|
-
return (
|
|
334
|
-
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
335
|
-
—
|
|
336
|
-
</Text>
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
const present = leaderSkills.has(expected);
|
|
340
|
-
if (present) {
|
|
341
|
-
return <Tag color="success">Assigned</Tag>;
|
|
342
|
-
}
|
|
343
|
-
return (
|
|
344
|
-
<Space size={4}>
|
|
345
|
-
<Tag icon={<WarningOutlined />} color="warning">
|
|
346
|
-
Missing
|
|
347
|
-
</Tag>
|
|
348
|
-
<Button
|
|
349
|
-
type="link"
|
|
350
|
-
size="small"
|
|
351
|
-
icon={<ThunderboltOutlined />}
|
|
352
|
-
onClick={() => handleAutoAssignSkill(record)}
|
|
353
|
-
>
|
|
354
|
-
Auto-add
|
|
355
|
-
</Button>
|
|
356
|
-
</Space>
|
|
357
|
-
);
|
|
358
|
-
},
|
|
359
|
-
},
|
|
360
|
-
{
|
|
361
|
-
title: 'Actions',
|
|
362
|
-
key: 'actions',
|
|
363
|
-
width: 160,
|
|
364
|
-
render: (_: any, record: any) => (
|
|
365
|
-
<Space>
|
|
366
|
-
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleOpen(record)}>
|
|
367
|
-
Edit
|
|
368
|
-
</Button>
|
|
369
|
-
<Popconfirm title="Delete this rule?" onConfirm={() => handleDelete(record.id)}>
|
|
370
|
-
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
|
371
|
-
Delete
|
|
372
|
-
</Button>
|
|
373
|
-
</Popconfirm>
|
|
374
|
-
</Space>
|
|
375
|
-
),
|
|
376
|
-
},
|
|
377
|
-
];
|
|
378
|
-
|
|
379
|
-
const leaderUsername = Form.useWatch('leaderUsername', form);
|
|
380
|
-
|
|
381
|
-
const missingSkillCount = React.useMemo(() => {
|
|
382
|
-
return rules.reduce((acc: number, r: any) => {
|
|
383
|
-
const leaderSkills = skillsMap.get(r.leaderUsername);
|
|
384
|
-
if (!leaderSkills) return acc;
|
|
385
|
-
const expected = expectedDelegateToolName(r.leaderUsername, r.subAgentUsername);
|
|
386
|
-
return leaderSkills.has(expected) ? acc : acc + 1;
|
|
387
|
-
}, 0);
|
|
388
|
-
}, [rules, skillsMap]);
|
|
389
|
-
|
|
390
|
-
const missingDispatchSkills = React.useMemo(() => {
|
|
391
|
-
return groupedRules
|
|
392
|
-
.map((group) => {
|
|
393
|
-
const leaderSkills = skillsMap.get(group.leaderUsername);
|
|
394
|
-
if (!leaderSkills) return null;
|
|
395
|
-
const toolName = expectedDispatchToolName(group.leaderUsername);
|
|
396
|
-
return leaderSkills.has(toolName)
|
|
397
|
-
? null
|
|
398
|
-
: { leaderUsername: group.leaderUsername, toolName, count: group.items.length };
|
|
399
|
-
})
|
|
400
|
-
.filter(Boolean) as Array<{ leaderUsername: string; toolName: string; count: number }>;
|
|
401
|
-
}, [groupedRules, skillsMap]);
|
|
402
|
-
|
|
403
|
-
const missingControllerSkills = React.useMemo(() => {
|
|
404
|
-
return groupedRules
|
|
405
|
-
.map((group) => {
|
|
406
|
-
const leaderSkills = skillsMap.get(group.leaderUsername);
|
|
407
|
-
if (!leaderSkills) return null;
|
|
408
|
-
const missing = controllerToolNames.filter((toolName) => !leaderSkills.has(toolName));
|
|
409
|
-
return missing.length ? { leaderUsername: group.leaderUsername, missing } : null;
|
|
410
|
-
})
|
|
411
|
-
.filter(Boolean) as Array<{ leaderUsername: string; missing: string[] }>;
|
|
412
|
-
}, [groupedRules, skillsMap]);
|
|
413
|
-
|
|
414
|
-
return (
|
|
415
|
-
<div>
|
|
416
|
-
<Alert
|
|
417
|
-
type="info"
|
|
418
|
-
showIcon
|
|
419
|
-
style={{ marginBottom: 16 }}
|
|
420
|
-
message="Orchestration Rules"
|
|
421
|
-
description={
|
|
422
|
-
<Text type="secondary">
|
|
423
|
-
Configure which AI Employees can act as Leaders (Orchestrators) and which ones they can delegate tasks to.
|
|
424
|
-
Each rule creates a callable tool for the Leader to invoke the Sub-Agent.
|
|
425
|
-
</Text>
|
|
426
|
-
}
|
|
427
|
-
/>
|
|
428
|
-
|
|
429
|
-
{missingSkillCount > 0 && (
|
|
430
|
-
<Alert
|
|
431
|
-
type="warning"
|
|
432
|
-
showIcon
|
|
433
|
-
style={{ marginBottom: 16 }}
|
|
434
|
-
message={`${missingSkillCount} rule${missingSkillCount > 1 ? 's' : ''} missing required skill assignment`}
|
|
435
|
-
description={
|
|
436
|
-
<Text type="secondary">
|
|
437
|
-
The Leader employee hasn't added the corresponding{' '}
|
|
438
|
-
<Text code>delegate_<leader>_to_<sub></Text> tool to its skillSettings, so the LLM cannot
|
|
439
|
-
actually call these sub-agents. Use the <b>Auto-add</b> button in the Skill column to fix.
|
|
440
|
-
</Text>
|
|
441
|
-
}
|
|
442
|
-
/>
|
|
443
|
-
)}
|
|
444
|
-
|
|
445
|
-
{missingControllerSkills.length > 0 && (
|
|
446
|
-
<Alert
|
|
447
|
-
type="warning"
|
|
448
|
-
showIcon
|
|
449
|
-
style={{ marginBottom: 16 }}
|
|
450
|
-
message={`${missingControllerSkills.length} leader${
|
|
451
|
-
missingControllerSkills.length > 1 ? 's' : ''
|
|
452
|
-
} missing orchestrator controller tools`}
|
|
453
|
-
description={
|
|
454
|
-
<Space direction="vertical" size={6}>
|
|
455
|
-
<Text type="secondary">
|
|
456
|
-
Leaders need the orchestrator controller tools to create an approval-first plan and execute it after
|
|
457
|
-
the user accepts the card.
|
|
458
|
-
</Text>
|
|
459
|
-
{missingControllerSkills.map(({ leaderUsername, missing }) => (
|
|
460
|
-
<Space key={leaderUsername} size={8} wrap>
|
|
461
|
-
<Tag color="blue">{employeeMap.get(leaderUsername) || leaderUsername}</Tag>
|
|
462
|
-
<Text type="secondary">{missing.length} missing</Text>
|
|
463
|
-
<Button
|
|
464
|
-
type="link"
|
|
465
|
-
size="small"
|
|
466
|
-
icon={<ThunderboltOutlined />}
|
|
467
|
-
onClick={() => handleAddSkillsToEmployee(leaderUsername, missing)}
|
|
468
|
-
>
|
|
469
|
-
Auto-add
|
|
470
|
-
</Button>
|
|
471
|
-
</Space>
|
|
472
|
-
))}
|
|
473
|
-
</Space>
|
|
474
|
-
}
|
|
475
|
-
/>
|
|
476
|
-
)}
|
|
477
|
-
|
|
478
|
-
{missingDispatchSkills.length > 0 && (
|
|
479
|
-
<Alert
|
|
480
|
-
type="warning"
|
|
481
|
-
showIcon
|
|
482
|
-
style={{ marginBottom: 16 }}
|
|
483
|
-
message={`${missingDispatchSkills.length} leader${
|
|
484
|
-
missingDispatchSkills.length > 1 ? 's' : ''
|
|
485
|
-
} missing dispatch skill assignment`}
|
|
486
|
-
description={
|
|
487
|
-
<Space direction="vertical" size={6}>
|
|
488
|
-
<Text type="secondary">
|
|
489
|
-
The fan-out tool lets a Leader dispatch multiple independent sub-tasks in one call. Add it to the
|
|
490
|
-
Leader's skills to enable the new multi-agent flow.
|
|
491
|
-
</Text>
|
|
492
|
-
{missingDispatchSkills.map(({ leaderUsername, toolName, count }) => (
|
|
493
|
-
<Space key={leaderUsername} size={8} wrap>
|
|
494
|
-
<Tag color="blue">{employeeMap.get(leaderUsername) || leaderUsername}</Tag>
|
|
495
|
-
<Text type="secondary">
|
|
496
|
-
{count} sub-agent{count > 1 ? 's' : ''}
|
|
497
|
-
</Text>
|
|
498
|
-
<Text code>{toolName}</Text>
|
|
499
|
-
<Button
|
|
500
|
-
type="link"
|
|
501
|
-
size="small"
|
|
502
|
-
icon={<ThunderboltOutlined />}
|
|
503
|
-
onClick={() => handleAutoAssignDispatchSkill(leaderUsername)}
|
|
504
|
-
>
|
|
505
|
-
Auto-add
|
|
506
|
-
</Button>
|
|
507
|
-
</Space>
|
|
508
|
-
))}
|
|
509
|
-
</Space>
|
|
510
|
-
}
|
|
511
|
-
/>
|
|
512
|
-
)}
|
|
513
|
-
|
|
514
|
-
{aliasConflicts.length > 0 && (
|
|
515
|
-
<Alert
|
|
516
|
-
type="warning"
|
|
517
|
-
showIcon
|
|
518
|
-
style={{ marginBottom: 16 }}
|
|
519
|
-
message="Legacy delegate_to_<sub> alias is no longer registered for these sub-agents"
|
|
520
|
-
description={
|
|
521
|
-
<Space direction="vertical" size={2}>
|
|
522
|
-
{aliasConflicts.map(({ sub, leaders }) => (
|
|
523
|
-
<Text key={sub} type="secondary">
|
|
524
|
-
<Tag color="green">{employeeMap.get(sub) || sub}</Tag>
|
|
525
|
-
has multiple leaders ({leaders.map((l) => employeeMap.get(l) || l).join(', ')}). The legacy alias is
|
|
526
|
-
dropped to avoid ambiguity — leaders must use <Text code>delegate_<leader>_to_<sub></Text>{' '}
|
|
527
|
-
in their skills.
|
|
528
|
-
</Text>
|
|
529
|
-
))}
|
|
530
|
-
</Space>
|
|
531
|
-
}
|
|
532
|
-
/>
|
|
533
|
-
)}
|
|
534
|
-
|
|
535
|
-
<Card bordered={false}>
|
|
536
|
-
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'flex-end' }}>
|
|
537
|
-
<Button type="primary" icon={<PlusOutlined />} onClick={() => handleOpen()}>
|
|
538
|
-
New Rule
|
|
539
|
-
</Button>
|
|
540
|
-
</div>
|
|
541
|
-
{groupedRules.length ? (
|
|
542
|
-
<Collapse
|
|
543
|
-
bordered={false}
|
|
544
|
-
defaultActiveKey={groupedRules.map((group) => group.leaderUsername)}
|
|
545
|
-
items={groupedRules.map((group) => ({
|
|
546
|
-
key: group.leaderUsername,
|
|
547
|
-
label: (
|
|
548
|
-
<Space>
|
|
549
|
-
<Tag color="blue">{employeeMap.get(group.leaderUsername) || group.leaderUsername}</Tag>
|
|
550
|
-
<Text type="secondary">
|
|
551
|
-
{group.items.length} sub-agent{group.items.length > 1 ? 's' : ''}
|
|
552
|
-
</Text>
|
|
553
|
-
{missingDispatchSkills.some((item) => item.leaderUsername === group.leaderUsername) && (
|
|
554
|
-
<Tag color="warning">Dispatch missing</Tag>
|
|
555
|
-
)}
|
|
556
|
-
</Space>
|
|
557
|
-
),
|
|
558
|
-
children: (
|
|
559
|
-
<Table
|
|
560
|
-
rowKey="id"
|
|
561
|
-
loading={loading}
|
|
562
|
-
dataSource={group.items}
|
|
563
|
-
columns={columns}
|
|
564
|
-
pagination={false}
|
|
565
|
-
size="middle"
|
|
566
|
-
/>
|
|
567
|
-
),
|
|
568
|
-
}))}
|
|
569
|
-
/>
|
|
570
|
-
) : (
|
|
571
|
-
<Table
|
|
572
|
-
rowKey="id"
|
|
573
|
-
loading={loading}
|
|
574
|
-
dataSource={[]}
|
|
575
|
-
columns={columns}
|
|
576
|
-
pagination={false}
|
|
577
|
-
size="middle"
|
|
578
|
-
locale={{ emptyText: <Empty description="No orchestration rules yet" /> }}
|
|
579
|
-
/>
|
|
580
|
-
)}
|
|
581
|
-
</Card>
|
|
582
|
-
|
|
583
|
-
<Drawer
|
|
584
|
-
title={editingRecord ? 'Edit Orchestration Rule' : 'New Orchestration Rule'}
|
|
585
|
-
width={480}
|
|
586
|
-
onClose={handleClose}
|
|
587
|
-
open={visible}
|
|
588
|
-
styles={{ body: { paddingBottom: 80 } }}
|
|
589
|
-
extra={
|
|
590
|
-
<Space>
|
|
591
|
-
<Button onClick={handleClose}>Cancel</Button>
|
|
592
|
-
<Button onClick={() => form.submit()} type="primary">
|
|
593
|
-
Save
|
|
594
|
-
</Button>
|
|
595
|
-
</Space>
|
|
596
|
-
}
|
|
597
|
-
>
|
|
598
|
-
<Form form={form} layout="vertical" onFinish={handleSave}>
|
|
599
|
-
<Form.Item
|
|
600
|
-
name="leaderUsername"
|
|
601
|
-
label="Leader (Orchestrator)"
|
|
602
|
-
rules={[{ required: true, message: 'Please select a Leader' }]}
|
|
603
|
-
tooltip="The AI Employee that will be able to delegate tasks to the Sub-Agent"
|
|
604
|
-
>
|
|
605
|
-
<AIEmployeeSelect placeholder="Select Leader AI Employee..." />
|
|
606
|
-
</Form.Item>
|
|
607
|
-
|
|
608
|
-
<Form.Item
|
|
609
|
-
name="subAgentUsername"
|
|
610
|
-
label="Sub-Agent"
|
|
611
|
-
rules={[{ required: true, message: 'Please select a Sub-Agent' }]}
|
|
612
|
-
tooltip="The AI Employee that will receive delegated tasks"
|
|
613
|
-
>
|
|
614
|
-
<AIEmployeeSelect placeholder="Select Sub-Agent AI Employee..." exclude={leaderUsername} />
|
|
615
|
-
</Form.Item>
|
|
616
|
-
|
|
617
|
-
<Form.Item
|
|
618
|
-
name="maxDepth"
|
|
619
|
-
label="Max Delegation Depth"
|
|
620
|
-
tooltip="How many layers of delegation are allowed (1 = leader calls sub-agent, sub-agent cannot delegate further)"
|
|
621
|
-
>
|
|
622
|
-
<InputNumber min={1} max={3} style={{ width: '100%' }} />
|
|
623
|
-
</Form.Item>
|
|
624
|
-
|
|
625
|
-
<Form.Item
|
|
626
|
-
name="timeout"
|
|
627
|
-
label="Timeout (ms)"
|
|
628
|
-
tooltip="Maximum time in milliseconds for the sub-agent to complete its task"
|
|
629
|
-
>
|
|
630
|
-
<InputNumber min={10000} max={600000} step={10000} style={{ width: '100%' }} />
|
|
631
|
-
</Form.Item>
|
|
632
|
-
|
|
633
|
-
<Form.Item
|
|
634
|
-
name="recursionLimit"
|
|
635
|
-
label="Recursion Limit"
|
|
636
|
-
tooltip="Max LangGraph reasoning steps per delegation. Higher = more complex multi-step tasks; lower = stricter cap on token usage. Default 50."
|
|
637
|
-
>
|
|
638
|
-
<InputNumber min={5} max={200} step={5} style={{ width: '100%' }} />
|
|
639
|
-
</Form.Item>
|
|
640
|
-
|
|
641
|
-
<Form.Item
|
|
642
|
-
name="harnessTag"
|
|
643
|
-
label="Harness Profile"
|
|
644
|
-
tooltip="Profile tag used by plan approval, controller limits, and orchestration policy."
|
|
645
|
-
>
|
|
646
|
-
<Select
|
|
647
|
-
loading={harnessLoading}
|
|
648
|
-
options={[
|
|
649
|
-
{ label: 'default', value: 'default' },
|
|
650
|
-
...harnessProfiles
|
|
651
|
-
.filter((profile: any) => profile.tag !== 'default')
|
|
652
|
-
.map((profile: any) => ({
|
|
653
|
-
label: profile.title ? `${profile.tag} - ${profile.title}` : profile.tag,
|
|
654
|
-
value: profile.tag,
|
|
655
|
-
})),
|
|
656
|
-
]}
|
|
657
|
-
/>
|
|
658
|
-
</Form.Item>
|
|
659
|
-
|
|
660
|
-
<Form.Item
|
|
661
|
-
name="llmService"
|
|
662
|
-
label="Override LLM Service"
|
|
663
|
-
tooltip="Optional: Provider name. Leave empty to inherit from Leader."
|
|
664
|
-
>
|
|
665
|
-
<Select
|
|
666
|
-
allowClear
|
|
667
|
-
placeholder="Inherit from Leader"
|
|
668
|
-
loading={llmLoading}
|
|
669
|
-
options={llmServices.map((svc: any) => ({
|
|
670
|
-
label: svc.llmServiceTitle || svc.llmService,
|
|
671
|
-
value: svc.llmService,
|
|
672
|
-
}))}
|
|
673
|
-
onChange={() => form.setFieldValue('model', undefined)}
|
|
674
|
-
/>
|
|
675
|
-
</Form.Item>
|
|
676
|
-
|
|
677
|
-
<Form.Item
|
|
678
|
-
noStyle
|
|
679
|
-
shouldUpdate={(prevValues, currentValues) => prevValues.llmService !== currentValues.llmService}
|
|
680
|
-
>
|
|
681
|
-
{() => {
|
|
682
|
-
const selectedServiceId = form.getFieldValue('llmService');
|
|
683
|
-
const selectedService = llmServices.find((s: any) => s.llmService === selectedServiceId);
|
|
684
|
-
const availableModels = Array.isArray(selectedService?.enabledModels)
|
|
685
|
-
? selectedService.enabledModels
|
|
686
|
-
: [];
|
|
687
|
-
|
|
688
|
-
return (
|
|
689
|
-
<Form.Item
|
|
690
|
-
name="model"
|
|
691
|
-
label="Override Model"
|
|
692
|
-
tooltip="Optional: Model name. Leave empty to inherit from Leader."
|
|
693
|
-
rules={[{ required: !!selectedServiceId, message: 'Please select a model' }]}
|
|
694
|
-
>
|
|
695
|
-
<Select
|
|
696
|
-
allowClear
|
|
697
|
-
placeholder={selectedServiceId ? 'Select a model' : 'Inherit from Leader'}
|
|
698
|
-
disabled={!selectedServiceId}
|
|
699
|
-
options={availableModels.map((m: any) => ({
|
|
700
|
-
label: m.label,
|
|
701
|
-
value: m.value,
|
|
702
|
-
}))}
|
|
703
|
-
/>
|
|
704
|
-
</Form.Item>
|
|
705
|
-
);
|
|
706
|
-
}}
|
|
707
|
-
</Form.Item>
|
|
708
|
-
|
|
709
|
-
<Form.Item name="enabled" label="Enabled" valuePropName="checked">
|
|
710
|
-
<Switch />
|
|
711
|
-
</Form.Item>
|
|
712
|
-
</Form>
|
|
713
|
-
</Drawer>
|
|
714
|
-
</div>
|
|
715
|
-
);
|
|
716
|
-
};
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Table,
|
|
4
|
+
Button,
|
|
5
|
+
Drawer,
|
|
6
|
+
Form,
|
|
7
|
+
InputNumber,
|
|
8
|
+
Switch,
|
|
9
|
+
Space,
|
|
10
|
+
Popconfirm,
|
|
11
|
+
Card,
|
|
12
|
+
message,
|
|
13
|
+
Tag,
|
|
14
|
+
Typography,
|
|
15
|
+
Alert,
|
|
16
|
+
Collapse,
|
|
17
|
+
Empty,
|
|
18
|
+
Select,
|
|
19
|
+
} from 'antd';
|
|
20
|
+
import {
|
|
21
|
+
PlusOutlined,
|
|
22
|
+
EditOutlined,
|
|
23
|
+
DeleteOutlined,
|
|
24
|
+
SwapRightOutlined,
|
|
25
|
+
WarningOutlined,
|
|
26
|
+
ThunderboltOutlined,
|
|
27
|
+
} from '@ant-design/icons';
|
|
28
|
+
import { useAPIClient, useRequest } from '@nocobase/client';
|
|
29
|
+
import { AIEmployeeSelect } from './AIEmployeeSelect';
|
|
30
|
+
import { useAIEmployees } from './AIEmployeesContext';
|
|
31
|
+
|
|
32
|
+
const { Text } = Typography;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Mirrors server-side `sanitizeToolPart` in delegate-task.ts so we can compute
|
|
36
|
+
* the expected delegation tool names here and detect when the leader hasn't
|
|
37
|
+
* added them to its skillSettings.
|
|
38
|
+
*/
|
|
39
|
+
const sanitizeToolPart = (value: string) => (value || '').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
40
|
+
const expectedDelegateToolName = (leader: string, sub: string) =>
|
|
41
|
+
`delegate_${sanitizeToolPart(leader)}_to_${sanitizeToolPart(sub)}`;
|
|
42
|
+
const expectedDispatchToolName = (leader: string) => `dispatch_subagents_${sanitizeToolPart(leader)}`;
|
|
43
|
+
const controllerToolNames = [
|
|
44
|
+
'orchestrator_plan_goal',
|
|
45
|
+
'orchestrator_execute_plan',
|
|
46
|
+
'orchestrator_status',
|
|
47
|
+
'orchestrator_cancel',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
export const RulesTab: React.FC = () => {
|
|
51
|
+
const api = useAPIClient();
|
|
52
|
+
const [visible, setVisible] = useState(false);
|
|
53
|
+
const [editingRecord, setEditingRecord] = useState<any>(null);
|
|
54
|
+
const [form] = Form.useForm();
|
|
55
|
+
|
|
56
|
+
const { data, loading, refresh } = useRequest({
|
|
57
|
+
url: 'orchestratorConfig:list',
|
|
58
|
+
params: {
|
|
59
|
+
sort: ['-createdAt'],
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const { data: llmServicesData, loading: llmLoading } = useRequest({
|
|
64
|
+
url: 'ai:listAllEnabledModels',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const { data: harnessProfilesData, loading: harnessLoading } = useRequest({
|
|
68
|
+
url: 'agentHarnessProfiles:list',
|
|
69
|
+
params: {
|
|
70
|
+
filter: { enabled: true },
|
|
71
|
+
sort: ['tag'],
|
|
72
|
+
pageSize: 100,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const llmServices = React.useMemo(() => {
|
|
77
|
+
const raw = (llmServicesData as any)?.data ?? llmServicesData;
|
|
78
|
+
if (Array.isArray(raw)) return raw;
|
|
79
|
+
return Array.isArray(raw?.data) ? raw.data : [];
|
|
80
|
+
}, [llmServicesData]);
|
|
81
|
+
|
|
82
|
+
const harnessProfiles = React.useMemo(() => {
|
|
83
|
+
const raw = (harnessProfilesData as any)?.data ?? harnessProfilesData;
|
|
84
|
+
if (Array.isArray(raw)) return raw;
|
|
85
|
+
return Array.isArray(raw?.data) ? raw.data : [];
|
|
86
|
+
}, [harnessProfilesData]);
|
|
87
|
+
|
|
88
|
+
// P3 FIX: Use shared context instead of duplicate API call
|
|
89
|
+
const { employeeMap, skillsMap, refresh: refreshEmployees } = useAIEmployees();
|
|
90
|
+
const rules = React.useMemo(() => {
|
|
91
|
+
const rows = (data as any)?.data;
|
|
92
|
+
return Array.isArray(rows) ? rows : [];
|
|
93
|
+
}, [data]);
|
|
94
|
+
|
|
95
|
+
const handleAddSkillsToEmployee = async (employeeUsername: string, toolNames: string[]) => {
|
|
96
|
+
try {
|
|
97
|
+
// Re-fetch the leader to merge its current skills (skillsMap may be stale).
|
|
98
|
+
const leaderResp = await api.request({
|
|
99
|
+
url: 'aiEmployees:get',
|
|
100
|
+
params: { filterByTk: employeeUsername },
|
|
101
|
+
});
|
|
102
|
+
const leader = (leaderResp as any)?.data?.data;
|
|
103
|
+
if (!leader) {
|
|
104
|
+
message.error('Could not load AI employee.');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const existing = Array.isArray(leader.skillSettings?.skills) ? leader.skillSettings.skills : [];
|
|
108
|
+
const existingNames = new Set(existing.map((s: any) => (typeof s === 'string' ? s : s?.name)));
|
|
109
|
+
const missing = toolNames.filter((toolName) => !existingNames.has(toolName));
|
|
110
|
+
if (!missing.length) {
|
|
111
|
+
message.info('Skills already present.');
|
|
112
|
+
await refreshEmployees();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const nextSkills = [...existing, ...missing.map((name) => ({ name, autoCall: false }))];
|
|
116
|
+
await api.request({
|
|
117
|
+
url: 'aiEmployees:update',
|
|
118
|
+
method: 'put',
|
|
119
|
+
params: { filterByTk: employeeUsername },
|
|
120
|
+
data: { skillSettings: { ...(leader.skillSettings || {}), skills: nextSkills } },
|
|
121
|
+
});
|
|
122
|
+
message.success(`Added ${missing.length} skill${missing.length > 1 ? 's' : ''} to ${employeeUsername}.`);
|
|
123
|
+
await refreshEmployees();
|
|
124
|
+
} catch (e: any) {
|
|
125
|
+
message.error(`Auto-assign failed: ${e?.message || 'unknown error'}`);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const handleAddSkillToEmployee = async (employeeUsername: string, toolName: string) => {
|
|
130
|
+
await handleAddSkillsToEmployee(employeeUsername, [toolName]);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const handleAutoAssignSkill = async (record: any) => {
|
|
134
|
+
await handleAddSkillToEmployee(
|
|
135
|
+
record.leaderUsername,
|
|
136
|
+
expectedDelegateToolName(record.leaderUsername, record.subAgentUsername),
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const handleAutoAssignDispatchSkill = async (leaderUsername: string) => {
|
|
141
|
+
await handleAddSkillToEmployee(leaderUsername, expectedDispatchToolName(leaderUsername));
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const subAgentLeaderCount = React.useMemo(() => {
|
|
145
|
+
const counts = new Map<string, Set<string>>();
|
|
146
|
+
for (const rule of rules) {
|
|
147
|
+
const set = counts.get(rule.subAgentUsername) || new Set<string>();
|
|
148
|
+
set.add(rule.leaderUsername);
|
|
149
|
+
counts.set(rule.subAgentUsername, set);
|
|
150
|
+
}
|
|
151
|
+
return counts;
|
|
152
|
+
}, [rules]);
|
|
153
|
+
|
|
154
|
+
const aliasConflicts = React.useMemo(() => {
|
|
155
|
+
return Array.from(subAgentLeaderCount.entries())
|
|
156
|
+
.filter(([, leaders]) => leaders.size > 1)
|
|
157
|
+
.map(([sub, leaders]) => ({ sub, leaders: Array.from(leaders) }));
|
|
158
|
+
}, [subAgentLeaderCount]);
|
|
159
|
+
|
|
160
|
+
const groupedRules = React.useMemo(() => {
|
|
161
|
+
const groups = new Map<string, any[]>();
|
|
162
|
+
for (const rule of rules) {
|
|
163
|
+
const key = rule.leaderUsername || 'unknown';
|
|
164
|
+
let items = groups.get(key);
|
|
165
|
+
if (!items) {
|
|
166
|
+
items = [];
|
|
167
|
+
groups.set(key, items);
|
|
168
|
+
}
|
|
169
|
+
items.push(rule);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return Array.from(groups.entries()).map(([leaderUsername, items]) => ({
|
|
173
|
+
leaderUsername,
|
|
174
|
+
items,
|
|
175
|
+
}));
|
|
176
|
+
}, [rules]);
|
|
177
|
+
|
|
178
|
+
const handleOpen = (record?: any) => {
|
|
179
|
+
setEditingRecord(record);
|
|
180
|
+
if (record) {
|
|
181
|
+
form.setFieldsValue(record);
|
|
182
|
+
} else {
|
|
183
|
+
form.resetFields();
|
|
184
|
+
form.setFieldsValue({ enabled: true, maxDepth: 1, timeout: 120000, recursionLimit: 50, harnessTag: 'default' });
|
|
185
|
+
}
|
|
186
|
+
setVisible(true);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const handleClose = () => {
|
|
190
|
+
setVisible(false);
|
|
191
|
+
setEditingRecord(null);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const handleSave = async (values: any) => {
|
|
195
|
+
// Validate: leader !== subAgent
|
|
196
|
+
if (values.leaderUsername === values.subAgentUsername) {
|
|
197
|
+
message.error('Leader and Sub-Agent cannot be the same employee.');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
if (editingRecord) {
|
|
203
|
+
await api.request({
|
|
204
|
+
url: 'orchestratorConfig:update',
|
|
205
|
+
method: 'put',
|
|
206
|
+
params: { filterByTk: editingRecord.id },
|
|
207
|
+
data: values,
|
|
208
|
+
});
|
|
209
|
+
message.success('Rule updated');
|
|
210
|
+
} else {
|
|
211
|
+
await api.request({
|
|
212
|
+
url: 'orchestratorConfig:create',
|
|
213
|
+
method: 'post',
|
|
214
|
+
data: values,
|
|
215
|
+
});
|
|
216
|
+
message.success('Rule created');
|
|
217
|
+
}
|
|
218
|
+
handleClose();
|
|
219
|
+
refresh();
|
|
220
|
+
} catch (e: any) {
|
|
221
|
+
const msg = e?.response?.data?.errors?.[0]?.message || e.message;
|
|
222
|
+
message.error(`Save failed: ${msg}`);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const handleDelete = async (id: string) => {
|
|
227
|
+
try {
|
|
228
|
+
await api.request({
|
|
229
|
+
url: 'orchestratorConfig:destroy',
|
|
230
|
+
method: 'delete',
|
|
231
|
+
params: { filterByTk: id },
|
|
232
|
+
});
|
|
233
|
+
message.success('Rule deleted');
|
|
234
|
+
refresh();
|
|
235
|
+
} catch (e: any) {
|
|
236
|
+
message.error(`Delete failed: ${e.message}`);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const columns = [
|
|
241
|
+
{
|
|
242
|
+
title: 'Leader (Orchestrator)',
|
|
243
|
+
dataIndex: 'leaderUsername',
|
|
244
|
+
key: 'leaderUsername',
|
|
245
|
+
render: (username: string) => <Tag color="blue">{employeeMap.get(username) || username}</Tag>,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
title: '',
|
|
249
|
+
key: 'arrow',
|
|
250
|
+
width: 50,
|
|
251
|
+
render: () => <SwapRightOutlined style={{ color: '#999', fontSize: 18 }} />,
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
title: 'Sub-Agent',
|
|
255
|
+
dataIndex: 'subAgentUsername',
|
|
256
|
+
key: 'subAgentUsername',
|
|
257
|
+
render: (username: string) => <Tag color="green">{employeeMap.get(username) || username}</Tag>,
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
title: 'Harness',
|
|
261
|
+
dataIndex: 'harnessTag',
|
|
262
|
+
key: 'harnessTag',
|
|
263
|
+
width: 120,
|
|
264
|
+
render: (tag: string) => <Tag color="purple">{tag || 'default'}</Tag>,
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
title: 'Max Depth',
|
|
268
|
+
dataIndex: 'maxDepth',
|
|
269
|
+
key: 'maxDepth',
|
|
270
|
+
width: 100,
|
|
271
|
+
render: (v: number) => v ?? 1,
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
title: 'Timeout',
|
|
275
|
+
dataIndex: 'timeout',
|
|
276
|
+
key: 'timeout',
|
|
277
|
+
width: 100,
|
|
278
|
+
render: (v: number) => `${((v ?? 120000) / 1000).toFixed(0)}s`,
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
title: 'LLM Override',
|
|
282
|
+
key: 'llmOverride',
|
|
283
|
+
width: 140,
|
|
284
|
+
render: (_: any, record: any) => {
|
|
285
|
+
if (record.llmService && record.model) {
|
|
286
|
+
const svc = llmServices.find((s: any) => s.llmService === record.llmService);
|
|
287
|
+
const svcName = svc ? svc.llmServiceTitle : record.llmService;
|
|
288
|
+
return (
|
|
289
|
+
<Space direction="vertical" size={0}>
|
|
290
|
+
<Text style={{ fontSize: 12 }}>{svcName}</Text>
|
|
291
|
+
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
292
|
+
{record.model}
|
|
293
|
+
</Text>
|
|
294
|
+
</Space>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
return (
|
|
298
|
+
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
299
|
+
Inherited
|
|
300
|
+
</Text>
|
|
301
|
+
);
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
title: 'Enabled',
|
|
306
|
+
dataIndex: 'enabled',
|
|
307
|
+
key: 'enabled',
|
|
308
|
+
width: 80,
|
|
309
|
+
render: (enabled: boolean, record: any) => (
|
|
310
|
+
<Switch
|
|
311
|
+
checked={enabled}
|
|
312
|
+
size="small"
|
|
313
|
+
onChange={async (checked) => {
|
|
314
|
+
await api.request({
|
|
315
|
+
url: 'orchestratorConfig:update',
|
|
316
|
+
method: 'put',
|
|
317
|
+
params: { filterByTk: record.id },
|
|
318
|
+
data: { enabled: checked },
|
|
319
|
+
});
|
|
320
|
+
refresh();
|
|
321
|
+
}}
|
|
322
|
+
/>
|
|
323
|
+
),
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
title: 'Skill',
|
|
327
|
+
key: 'skill',
|
|
328
|
+
width: 150,
|
|
329
|
+
render: (_: any, record: any) => {
|
|
330
|
+
const expected = expectedDelegateToolName(record.leaderUsername, record.subAgentUsername);
|
|
331
|
+
const leaderSkills = skillsMap.get(record.leaderUsername);
|
|
332
|
+
if (!leaderSkills) {
|
|
333
|
+
return (
|
|
334
|
+
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
335
|
+
—
|
|
336
|
+
</Text>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
const present = leaderSkills.has(expected);
|
|
340
|
+
if (present) {
|
|
341
|
+
return <Tag color="success">Assigned</Tag>;
|
|
342
|
+
}
|
|
343
|
+
return (
|
|
344
|
+
<Space size={4}>
|
|
345
|
+
<Tag icon={<WarningOutlined />} color="warning">
|
|
346
|
+
Missing
|
|
347
|
+
</Tag>
|
|
348
|
+
<Button
|
|
349
|
+
type="link"
|
|
350
|
+
size="small"
|
|
351
|
+
icon={<ThunderboltOutlined />}
|
|
352
|
+
onClick={() => handleAutoAssignSkill(record)}
|
|
353
|
+
>
|
|
354
|
+
Auto-add
|
|
355
|
+
</Button>
|
|
356
|
+
</Space>
|
|
357
|
+
);
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
title: 'Actions',
|
|
362
|
+
key: 'actions',
|
|
363
|
+
width: 160,
|
|
364
|
+
render: (_: any, record: any) => (
|
|
365
|
+
<Space>
|
|
366
|
+
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleOpen(record)}>
|
|
367
|
+
Edit
|
|
368
|
+
</Button>
|
|
369
|
+
<Popconfirm title="Delete this rule?" onConfirm={() => handleDelete(record.id)}>
|
|
370
|
+
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
|
371
|
+
Delete
|
|
372
|
+
</Button>
|
|
373
|
+
</Popconfirm>
|
|
374
|
+
</Space>
|
|
375
|
+
),
|
|
376
|
+
},
|
|
377
|
+
];
|
|
378
|
+
|
|
379
|
+
const leaderUsername = Form.useWatch('leaderUsername', form);
|
|
380
|
+
|
|
381
|
+
const missingSkillCount = React.useMemo(() => {
|
|
382
|
+
return rules.reduce((acc: number, r: any) => {
|
|
383
|
+
const leaderSkills = skillsMap.get(r.leaderUsername);
|
|
384
|
+
if (!leaderSkills) return acc;
|
|
385
|
+
const expected = expectedDelegateToolName(r.leaderUsername, r.subAgentUsername);
|
|
386
|
+
return leaderSkills.has(expected) ? acc : acc + 1;
|
|
387
|
+
}, 0);
|
|
388
|
+
}, [rules, skillsMap]);
|
|
389
|
+
|
|
390
|
+
const missingDispatchSkills = React.useMemo(() => {
|
|
391
|
+
return groupedRules
|
|
392
|
+
.map((group) => {
|
|
393
|
+
const leaderSkills = skillsMap.get(group.leaderUsername);
|
|
394
|
+
if (!leaderSkills) return null;
|
|
395
|
+
const toolName = expectedDispatchToolName(group.leaderUsername);
|
|
396
|
+
return leaderSkills.has(toolName)
|
|
397
|
+
? null
|
|
398
|
+
: { leaderUsername: group.leaderUsername, toolName, count: group.items.length };
|
|
399
|
+
})
|
|
400
|
+
.filter(Boolean) as Array<{ leaderUsername: string; toolName: string; count: number }>;
|
|
401
|
+
}, [groupedRules, skillsMap]);
|
|
402
|
+
|
|
403
|
+
const missingControllerSkills = React.useMemo(() => {
|
|
404
|
+
return groupedRules
|
|
405
|
+
.map((group) => {
|
|
406
|
+
const leaderSkills = skillsMap.get(group.leaderUsername);
|
|
407
|
+
if (!leaderSkills) return null;
|
|
408
|
+
const missing = controllerToolNames.filter((toolName) => !leaderSkills.has(toolName));
|
|
409
|
+
return missing.length ? { leaderUsername: group.leaderUsername, missing } : null;
|
|
410
|
+
})
|
|
411
|
+
.filter(Boolean) as Array<{ leaderUsername: string; missing: string[] }>;
|
|
412
|
+
}, [groupedRules, skillsMap]);
|
|
413
|
+
|
|
414
|
+
return (
|
|
415
|
+
<div>
|
|
416
|
+
<Alert
|
|
417
|
+
type="info"
|
|
418
|
+
showIcon
|
|
419
|
+
style={{ marginBottom: 16 }}
|
|
420
|
+
message="Orchestration Rules"
|
|
421
|
+
description={
|
|
422
|
+
<Text type="secondary">
|
|
423
|
+
Configure which AI Employees can act as Leaders (Orchestrators) and which ones they can delegate tasks to.
|
|
424
|
+
Each rule creates a callable tool for the Leader to invoke the Sub-Agent.
|
|
425
|
+
</Text>
|
|
426
|
+
}
|
|
427
|
+
/>
|
|
428
|
+
|
|
429
|
+
{missingSkillCount > 0 && (
|
|
430
|
+
<Alert
|
|
431
|
+
type="warning"
|
|
432
|
+
showIcon
|
|
433
|
+
style={{ marginBottom: 16 }}
|
|
434
|
+
message={`${missingSkillCount} rule${missingSkillCount > 1 ? 's' : ''} missing required skill assignment`}
|
|
435
|
+
description={
|
|
436
|
+
<Text type="secondary">
|
|
437
|
+
The Leader employee hasn't added the corresponding{' '}
|
|
438
|
+
<Text code>delegate_<leader>_to_<sub></Text> tool to its skillSettings, so the LLM cannot
|
|
439
|
+
actually call these sub-agents. Use the <b>Auto-add</b> button in the Skill column to fix.
|
|
440
|
+
</Text>
|
|
441
|
+
}
|
|
442
|
+
/>
|
|
443
|
+
)}
|
|
444
|
+
|
|
445
|
+
{missingControllerSkills.length > 0 && (
|
|
446
|
+
<Alert
|
|
447
|
+
type="warning"
|
|
448
|
+
showIcon
|
|
449
|
+
style={{ marginBottom: 16 }}
|
|
450
|
+
message={`${missingControllerSkills.length} leader${
|
|
451
|
+
missingControllerSkills.length > 1 ? 's' : ''
|
|
452
|
+
} missing orchestrator controller tools`}
|
|
453
|
+
description={
|
|
454
|
+
<Space direction="vertical" size={6}>
|
|
455
|
+
<Text type="secondary">
|
|
456
|
+
Leaders need the orchestrator controller tools to create an approval-first plan and execute it after
|
|
457
|
+
the user accepts the card.
|
|
458
|
+
</Text>
|
|
459
|
+
{missingControllerSkills.map(({ leaderUsername, missing }) => (
|
|
460
|
+
<Space key={leaderUsername} size={8} wrap>
|
|
461
|
+
<Tag color="blue">{employeeMap.get(leaderUsername) || leaderUsername}</Tag>
|
|
462
|
+
<Text type="secondary">{missing.length} missing</Text>
|
|
463
|
+
<Button
|
|
464
|
+
type="link"
|
|
465
|
+
size="small"
|
|
466
|
+
icon={<ThunderboltOutlined />}
|
|
467
|
+
onClick={() => handleAddSkillsToEmployee(leaderUsername, missing)}
|
|
468
|
+
>
|
|
469
|
+
Auto-add
|
|
470
|
+
</Button>
|
|
471
|
+
</Space>
|
|
472
|
+
))}
|
|
473
|
+
</Space>
|
|
474
|
+
}
|
|
475
|
+
/>
|
|
476
|
+
)}
|
|
477
|
+
|
|
478
|
+
{missingDispatchSkills.length > 0 && (
|
|
479
|
+
<Alert
|
|
480
|
+
type="warning"
|
|
481
|
+
showIcon
|
|
482
|
+
style={{ marginBottom: 16 }}
|
|
483
|
+
message={`${missingDispatchSkills.length} leader${
|
|
484
|
+
missingDispatchSkills.length > 1 ? 's' : ''
|
|
485
|
+
} missing dispatch skill assignment`}
|
|
486
|
+
description={
|
|
487
|
+
<Space direction="vertical" size={6}>
|
|
488
|
+
<Text type="secondary">
|
|
489
|
+
The fan-out tool lets a Leader dispatch multiple independent sub-tasks in one call. Add it to the
|
|
490
|
+
Leader's skills to enable the new multi-agent flow.
|
|
491
|
+
</Text>
|
|
492
|
+
{missingDispatchSkills.map(({ leaderUsername, toolName, count }) => (
|
|
493
|
+
<Space key={leaderUsername} size={8} wrap>
|
|
494
|
+
<Tag color="blue">{employeeMap.get(leaderUsername) || leaderUsername}</Tag>
|
|
495
|
+
<Text type="secondary">
|
|
496
|
+
{count} sub-agent{count > 1 ? 's' : ''}
|
|
497
|
+
</Text>
|
|
498
|
+
<Text code>{toolName}</Text>
|
|
499
|
+
<Button
|
|
500
|
+
type="link"
|
|
501
|
+
size="small"
|
|
502
|
+
icon={<ThunderboltOutlined />}
|
|
503
|
+
onClick={() => handleAutoAssignDispatchSkill(leaderUsername)}
|
|
504
|
+
>
|
|
505
|
+
Auto-add
|
|
506
|
+
</Button>
|
|
507
|
+
</Space>
|
|
508
|
+
))}
|
|
509
|
+
</Space>
|
|
510
|
+
}
|
|
511
|
+
/>
|
|
512
|
+
)}
|
|
513
|
+
|
|
514
|
+
{aliasConflicts.length > 0 && (
|
|
515
|
+
<Alert
|
|
516
|
+
type="warning"
|
|
517
|
+
showIcon
|
|
518
|
+
style={{ marginBottom: 16 }}
|
|
519
|
+
message="Legacy delegate_to_<sub> alias is no longer registered for these sub-agents"
|
|
520
|
+
description={
|
|
521
|
+
<Space direction="vertical" size={2}>
|
|
522
|
+
{aliasConflicts.map(({ sub, leaders }) => (
|
|
523
|
+
<Text key={sub} type="secondary">
|
|
524
|
+
<Tag color="green">{employeeMap.get(sub) || sub}</Tag>
|
|
525
|
+
has multiple leaders ({leaders.map((l) => employeeMap.get(l) || l).join(', ')}). The legacy alias is
|
|
526
|
+
dropped to avoid ambiguity — leaders must use <Text code>delegate_<leader>_to_<sub></Text>{' '}
|
|
527
|
+
in their skills.
|
|
528
|
+
</Text>
|
|
529
|
+
))}
|
|
530
|
+
</Space>
|
|
531
|
+
}
|
|
532
|
+
/>
|
|
533
|
+
)}
|
|
534
|
+
|
|
535
|
+
<Card bordered={false}>
|
|
536
|
+
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'flex-end' }}>
|
|
537
|
+
<Button type="primary" icon={<PlusOutlined />} onClick={() => handleOpen()}>
|
|
538
|
+
New Rule
|
|
539
|
+
</Button>
|
|
540
|
+
</div>
|
|
541
|
+
{groupedRules.length ? (
|
|
542
|
+
<Collapse
|
|
543
|
+
bordered={false}
|
|
544
|
+
defaultActiveKey={groupedRules.map((group) => group.leaderUsername)}
|
|
545
|
+
items={groupedRules.map((group) => ({
|
|
546
|
+
key: group.leaderUsername,
|
|
547
|
+
label: (
|
|
548
|
+
<Space>
|
|
549
|
+
<Tag color="blue">{employeeMap.get(group.leaderUsername) || group.leaderUsername}</Tag>
|
|
550
|
+
<Text type="secondary">
|
|
551
|
+
{group.items.length} sub-agent{group.items.length > 1 ? 's' : ''}
|
|
552
|
+
</Text>
|
|
553
|
+
{missingDispatchSkills.some((item) => item.leaderUsername === group.leaderUsername) && (
|
|
554
|
+
<Tag color="warning">Dispatch missing</Tag>
|
|
555
|
+
)}
|
|
556
|
+
</Space>
|
|
557
|
+
),
|
|
558
|
+
children: (
|
|
559
|
+
<Table
|
|
560
|
+
rowKey="id"
|
|
561
|
+
loading={loading}
|
|
562
|
+
dataSource={group.items}
|
|
563
|
+
columns={columns}
|
|
564
|
+
pagination={false}
|
|
565
|
+
size="middle"
|
|
566
|
+
/>
|
|
567
|
+
),
|
|
568
|
+
}))}
|
|
569
|
+
/>
|
|
570
|
+
) : (
|
|
571
|
+
<Table
|
|
572
|
+
rowKey="id"
|
|
573
|
+
loading={loading}
|
|
574
|
+
dataSource={[]}
|
|
575
|
+
columns={columns}
|
|
576
|
+
pagination={false}
|
|
577
|
+
size="middle"
|
|
578
|
+
locale={{ emptyText: <Empty description="No orchestration rules yet" /> }}
|
|
579
|
+
/>
|
|
580
|
+
)}
|
|
581
|
+
</Card>
|
|
582
|
+
|
|
583
|
+
<Drawer
|
|
584
|
+
title={editingRecord ? 'Edit Orchestration Rule' : 'New Orchestration Rule'}
|
|
585
|
+
width={480}
|
|
586
|
+
onClose={handleClose}
|
|
587
|
+
open={visible}
|
|
588
|
+
styles={{ body: { paddingBottom: 80 } }}
|
|
589
|
+
extra={
|
|
590
|
+
<Space>
|
|
591
|
+
<Button onClick={handleClose}>Cancel</Button>
|
|
592
|
+
<Button onClick={() => form.submit()} type="primary">
|
|
593
|
+
Save
|
|
594
|
+
</Button>
|
|
595
|
+
</Space>
|
|
596
|
+
}
|
|
597
|
+
>
|
|
598
|
+
<Form form={form} layout="vertical" onFinish={handleSave}>
|
|
599
|
+
<Form.Item
|
|
600
|
+
name="leaderUsername"
|
|
601
|
+
label="Leader (Orchestrator)"
|
|
602
|
+
rules={[{ required: true, message: 'Please select a Leader' }]}
|
|
603
|
+
tooltip="The AI Employee that will be able to delegate tasks to the Sub-Agent"
|
|
604
|
+
>
|
|
605
|
+
<AIEmployeeSelect placeholder="Select Leader AI Employee..." />
|
|
606
|
+
</Form.Item>
|
|
607
|
+
|
|
608
|
+
<Form.Item
|
|
609
|
+
name="subAgentUsername"
|
|
610
|
+
label="Sub-Agent"
|
|
611
|
+
rules={[{ required: true, message: 'Please select a Sub-Agent' }]}
|
|
612
|
+
tooltip="The AI Employee that will receive delegated tasks"
|
|
613
|
+
>
|
|
614
|
+
<AIEmployeeSelect placeholder="Select Sub-Agent AI Employee..." exclude={leaderUsername} />
|
|
615
|
+
</Form.Item>
|
|
616
|
+
|
|
617
|
+
<Form.Item
|
|
618
|
+
name="maxDepth"
|
|
619
|
+
label="Max Delegation Depth"
|
|
620
|
+
tooltip="How many layers of delegation are allowed (1 = leader calls sub-agent, sub-agent cannot delegate further)"
|
|
621
|
+
>
|
|
622
|
+
<InputNumber min={1} max={3} style={{ width: '100%' }} />
|
|
623
|
+
</Form.Item>
|
|
624
|
+
|
|
625
|
+
<Form.Item
|
|
626
|
+
name="timeout"
|
|
627
|
+
label="Timeout (ms)"
|
|
628
|
+
tooltip="Maximum time in milliseconds for the sub-agent to complete its task"
|
|
629
|
+
>
|
|
630
|
+
<InputNumber min={10000} max={600000} step={10000} style={{ width: '100%' }} />
|
|
631
|
+
</Form.Item>
|
|
632
|
+
|
|
633
|
+
<Form.Item
|
|
634
|
+
name="recursionLimit"
|
|
635
|
+
label="Recursion Limit"
|
|
636
|
+
tooltip="Max LangGraph reasoning steps per delegation. Higher = more complex multi-step tasks; lower = stricter cap on token usage. Default 50."
|
|
637
|
+
>
|
|
638
|
+
<InputNumber min={5} max={200} step={5} style={{ width: '100%' }} />
|
|
639
|
+
</Form.Item>
|
|
640
|
+
|
|
641
|
+
<Form.Item
|
|
642
|
+
name="harnessTag"
|
|
643
|
+
label="Harness Profile"
|
|
644
|
+
tooltip="Profile tag used by plan approval, controller limits, and orchestration policy."
|
|
645
|
+
>
|
|
646
|
+
<Select
|
|
647
|
+
loading={harnessLoading}
|
|
648
|
+
options={[
|
|
649
|
+
{ label: 'default', value: 'default' },
|
|
650
|
+
...harnessProfiles
|
|
651
|
+
.filter((profile: any) => profile.tag !== 'default')
|
|
652
|
+
.map((profile: any) => ({
|
|
653
|
+
label: profile.title ? `${profile.tag} - ${profile.title}` : profile.tag,
|
|
654
|
+
value: profile.tag,
|
|
655
|
+
})),
|
|
656
|
+
]}
|
|
657
|
+
/>
|
|
658
|
+
</Form.Item>
|
|
659
|
+
|
|
660
|
+
<Form.Item
|
|
661
|
+
name="llmService"
|
|
662
|
+
label="Override LLM Service"
|
|
663
|
+
tooltip="Optional: Provider name. Leave empty to inherit from Leader."
|
|
664
|
+
>
|
|
665
|
+
<Select
|
|
666
|
+
allowClear
|
|
667
|
+
placeholder="Inherit from Leader"
|
|
668
|
+
loading={llmLoading}
|
|
669
|
+
options={llmServices.map((svc: any) => ({
|
|
670
|
+
label: svc.llmServiceTitle || svc.llmService,
|
|
671
|
+
value: svc.llmService,
|
|
672
|
+
}))}
|
|
673
|
+
onChange={() => form.setFieldValue('model', undefined)}
|
|
674
|
+
/>
|
|
675
|
+
</Form.Item>
|
|
676
|
+
|
|
677
|
+
<Form.Item
|
|
678
|
+
noStyle
|
|
679
|
+
shouldUpdate={(prevValues, currentValues) => prevValues.llmService !== currentValues.llmService}
|
|
680
|
+
>
|
|
681
|
+
{() => {
|
|
682
|
+
const selectedServiceId = form.getFieldValue('llmService');
|
|
683
|
+
const selectedService = llmServices.find((s: any) => s.llmService === selectedServiceId);
|
|
684
|
+
const availableModels = Array.isArray(selectedService?.enabledModels)
|
|
685
|
+
? selectedService.enabledModels
|
|
686
|
+
: [];
|
|
687
|
+
|
|
688
|
+
return (
|
|
689
|
+
<Form.Item
|
|
690
|
+
name="model"
|
|
691
|
+
label="Override Model"
|
|
692
|
+
tooltip="Optional: Model name. Leave empty to inherit from Leader."
|
|
693
|
+
rules={[{ required: !!selectedServiceId, message: 'Please select a model' }]}
|
|
694
|
+
>
|
|
695
|
+
<Select
|
|
696
|
+
allowClear
|
|
697
|
+
placeholder={selectedServiceId ? 'Select a model' : 'Inherit from Leader'}
|
|
698
|
+
disabled={!selectedServiceId}
|
|
699
|
+
options={availableModels.map((m: any) => ({
|
|
700
|
+
label: m.label,
|
|
701
|
+
value: m.value,
|
|
702
|
+
}))}
|
|
703
|
+
/>
|
|
704
|
+
</Form.Item>
|
|
705
|
+
);
|
|
706
|
+
}}
|
|
707
|
+
</Form.Item>
|
|
708
|
+
|
|
709
|
+
<Form.Item name="enabled" label="Enabled" valuePropName="checked">
|
|
710
|
+
<Switch />
|
|
711
|
+
</Form.Item>
|
|
712
|
+
</Form>
|
|
713
|
+
</Drawer>
|
|
714
|
+
</div>
|
|
715
|
+
);
|
|
716
|
+
};
|