plugin-agent-orchestrator 1.0.22 → 1.0.25

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.
Files changed (103) hide show
  1. package/client-v2.d.ts +2 -0
  2. package/client-v2.js +1 -0
  3. package/dist/client/index.js +1 -1
  4. package/dist/client-v2/214.723affb37c13bf7a.js +10 -0
  5. package/dist/client-v2/264.0533912e6c5ea2d7.js +10 -0
  6. package/dist/client-v2/41.1805b2edfaa4afe2.js +10 -0
  7. package/dist/client-v2/418.5ae055abf141820e.js +10 -0
  8. package/dist/client-v2/619.d99d3c9e61c99064.js +10 -0
  9. package/dist/client-v2/70.a15d7fcec7c41768.js +10 -0
  10. package/dist/client-v2/892.72db4161511c8a16.js +10 -0
  11. package/dist/client-v2/926.87f660b670d85bcc.js +10 -0
  12. package/dist/client-v2/index.js +10 -0
  13. package/dist/externalVersion.js +8 -6
  14. package/dist/locale/en-US.json +7 -0
  15. package/dist/locale/vi-VN.json +7 -0
  16. package/dist/locale/zh-CN.json +27 -0
  17. package/dist/server/migrations/20260615000000-normalize-ai-employee-tool-bindings.js +63 -0
  18. package/dist/server/plugin.js +32 -1
  19. package/dist/server/services/AgentHarness.js +52 -27
  20. package/dist/server/services/AgentLoopController.js +8 -2
  21. package/dist/server/services/AgentLoopService.js +1 -1
  22. package/dist/server/services/AgentRegistryService.js +53 -42
  23. package/dist/server/services/CircuitBreaker.js +7 -2
  24. package/dist/server/services/CodeValidator.js +48 -14
  25. package/dist/server/services/SandboxRunner.js +18 -14
  26. package/dist/server/skill-hub/plugin.js +44 -17
  27. package/dist/server/tools/delegate-task.js +7 -2
  28. package/dist/server/tools/skill-execute.js +33 -2
  29. package/dist/server/utils/ai-manager.js +51 -0
  30. package/dist/server/utils/ctx-utils.js +11 -0
  31. package/dist/server/utils/skill-settings.js +122 -0
  32. package/package.json +49 -45
  33. package/src/client/AIEmployeesContext.tsx +60 -19
  34. package/src/client/AgentRunsTab.tsx +769 -764
  35. package/src/client/HarnessProfilesTab.tsx +257 -247
  36. package/src/client/RulesTab.tsx +787 -716
  37. package/src/client/TracingTab.tsx +9 -6
  38. package/src/client/plugin.tsx +34 -27
  39. package/src/client/skill-hub/components/ExecutionHistory.tsx +9 -8
  40. package/src/client/skill-hub/components/GitSkillImport.tsx +12 -5
  41. package/src/client/skill-hub/components/LoopSettings.tsx +2 -2
  42. package/src/client/skill-hub/components/SkillEditor.tsx +2 -2
  43. package/src/client/skill-hub/components/SkillManager.tsx +2 -2
  44. package/src/client/skill-hub/components/SkillMetrics.tsx +157 -124
  45. package/src/client/skill-hub/components/SkillTestPanel.tsx +14 -13
  46. package/src/client/skill-hub/index.tsx +58 -51
  47. package/src/client/skill-hub/locale.ts +1 -1
  48. package/src/client/skill-hub/tools/InteractionSchemasProvider.tsx +132 -99
  49. package/src/client/skill-hub/tools/registerSkillLoopCards.ts +71 -58
  50. package/src/client/tools/PlanApprovalCard.tsx +3 -2
  51. package/src/client/tools/registerOrchestratorCards.ts +17 -7
  52. package/src/client-v2/components/AIEmployeeSelect.tsx +47 -0
  53. package/src/client-v2/components/AIEmployeesContext.tsx +110 -0
  54. package/src/client-v2/components/AgentRunsTab.tsx +767 -0
  55. package/src/client-v2/components/HarnessProfilesTab.tsx +254 -0
  56. package/src/client-v2/components/RulesTab.tsx +782 -0
  57. package/src/client-v2/components/TracingTab.tsx +432 -0
  58. package/src/client-v2/hooks/useApiRequest.ts +114 -0
  59. package/src/client-v2/index.tsx +1 -0
  60. package/src/client-v2/pages/AgentRunsPage.tsx +13 -0
  61. package/src/client-v2/pages/ExecutionHistoryPage.tsx +10 -0
  62. package/src/client-v2/pages/HarnessProfilesPage.tsx +10 -0
  63. package/src/client-v2/pages/LoopSettingsPage.tsx +10 -0
  64. package/src/client-v2/pages/RulesPage.tsx +13 -0
  65. package/src/client-v2/pages/SkillDefinitionsPage.tsx +10 -0
  66. package/src/client-v2/pages/SkillMetricsPage.tsx +10 -0
  67. package/src/client-v2/pages/TracingPage.tsx +13 -0
  68. package/src/client-v2/plugin.tsx +70 -0
  69. package/src/client-v2/skill-hub/components/ExecutionHistory.tsx +196 -0
  70. package/src/client-v2/skill-hub/components/FileLinkList.tsx +37 -0
  71. package/src/client-v2/skill-hub/components/GitSkillImport.tsx +539 -0
  72. package/src/client-v2/skill-hub/components/LoopSettings.tsx +331 -0
  73. package/src/client-v2/skill-hub/components/SkillEditor.tsx +453 -0
  74. package/src/client-v2/skill-hub/components/SkillManager.tsx +174 -0
  75. package/src/client-v2/skill-hub/components/SkillMetrics.tsx +157 -0
  76. package/src/client-v2/skill-hub/components/SkillTestPanel.tsx +135 -0
  77. package/src/client-v2/skill-hub/locale.ts +13 -0
  78. package/src/client-v2/skill-hub/tools/loopTemplates.ts +52 -0
  79. package/src/client-v2/skill-hub/utils/jsonFields.ts +41 -0
  80. package/src/client-v2/utils/jsonFields.ts +41 -0
  81. package/src/locale/en-US.json +7 -0
  82. package/src/locale/vi-VN.json +7 -0
  83. package/src/locale/zh-CN.json +27 -0
  84. package/src/server/__tests__/agent-registry-service.test.ts +147 -0
  85. package/src/server/__tests__/code-validator.test.ts +63 -0
  86. package/src/server/__tests__/skill-execute.test.ts +33 -0
  87. package/src/server/__tests__/skill-settings.test.ts +63 -0
  88. package/src/server/migrations/20260615000000-normalize-ai-employee-tool-bindings.ts +39 -0
  89. package/src/server/plugin.ts +62 -21
  90. package/src/server/services/AgentHarness.ts +49 -22
  91. package/src/server/services/AgentLoopController.ts +17 -6
  92. package/src/server/services/AgentLoopService.ts +1 -1
  93. package/src/server/services/AgentPlannerService.ts +10 -0
  94. package/src/server/services/AgentRegistryService.ts +89 -47
  95. package/src/server/services/CircuitBreaker.ts +10 -0
  96. package/src/server/services/CodeValidator.ts +237 -159
  97. package/src/server/services/SandboxRunner.ts +203 -189
  98. package/src/server/skill-hub/plugin.ts +933 -898
  99. package/src/server/tools/delegate-task.ts +12 -9
  100. package/src/server/tools/skill-execute.ts +194 -160
  101. package/src/server/utils/ai-manager.ts +24 -0
  102. package/src/server/utils/ctx-utils.ts +14 -0
  103. package/src/server/utils/skill-settings.ts +116 -0
@@ -6,7 +6,7 @@ import { createReactAgent } from '@langchain/langgraph/prebuilt';
6
6
  import { DynamicStructuredTool } from '@langchain/core/tools';
7
7
  import { HumanMessage, SystemMessage } from '@langchain/core/messages';
8
8
  import type PluginAIServer from '@nocobase/plugin-ai/dist/server';
9
- import type { ToolsEntry } from '@nocobase/ai';
9
+ import type { ToolsEntry, ToolsRuntime } from '@nocobase/ai';
10
10
  import {
11
11
  ExecutionSpanService,
12
12
  getOrchestratorTraceContext,
@@ -39,10 +39,11 @@ type AgentExecutionResult = {
39
39
  messages: any[];
40
40
  };
41
41
 
42
- type EmployeeSkillConfig = {
43
- name: string;
44
- autoCall: boolean;
45
- };
42
+ type ToolRuntimeInput = string | ToolsRuntime | undefined;
43
+
44
+ function getToolCallId(runtime: ToolRuntimeInput) {
45
+ return typeof runtime === 'string' ? runtime : runtime?.toolCallId;
46
+ }
46
47
 
47
48
  function sanitizeToolPart(value: string) {
48
49
  return (value || '').replace(/[^a-zA-Z0-9_-]/g, '_');
@@ -154,7 +155,8 @@ function createDelegateToolOptions(
154
155
  .describe('Optional additional context to help the sub-agent understand the task better.'),
155
156
  }),
156
157
  },
157
- invoke: async (ctx: Context, args: { task: string; context?: string }, id: string) => {
158
+ invoke: async (ctx: Context, args: { task: string; context?: string }, runtime?: ToolRuntimeInput) => {
159
+ const id = getToolCallId(runtime) || `delegate-${Date.now()}`;
158
160
  const callingEmployee = await resolveCallingEmployee(ctx, plugin);
159
161
  if (!callingEmployee) {
160
162
  await logDelegation(ctx, plugin, {
@@ -312,8 +314,9 @@ function createDispatchToolOptions(
312
314
  invoke: async (
313
315
  ctx: Context,
314
316
  args: { tasks: Array<{ subAgent: string; task: string; context?: string }> },
315
- id: string,
317
+ runtime?: ToolRuntimeInput,
316
318
  ) => {
319
+ const id = getToolCallId(runtime) || `dispatch-${Date.now()}`;
317
320
  const callingEmployee = await resolveCallingEmployee(ctx, plugin);
318
321
  if (!callingEmployee) {
319
322
  const distinctSubs = Array.from(new Set((args.tasks ?? []).map((t) => t.subAgent).filter(Boolean)));
@@ -706,9 +709,9 @@ export function invalidateDelegateToolsCache() {
706
709
  *
707
710
  * Tool resolution uses the CORE toolsManager (app.aiManager.toolsManager) —
708
711
  * the same manager that AIEmployee.getToolsMap() uses (see ai-employee.ts:1286).
709
- * This ensures tool names in skillSettings.skills[].name match correctly.
712
+ * This ensures tool names in skillSettings.tools[].name match correctly.
710
713
  *
711
- * skillSettings.skills shape (verified against ai-employee.ts:1028):
714
+ * skillSettings.tools shape:
712
715
  * { name: string, autoCall: boolean }[]
713
716
  */
714
717
  async function invokeDelegateTask(
@@ -1,160 +1,194 @@
1
- import { parseJsonText } from '../skill-hub/utils/json-fields';
2
-
3
- export function createSkillExecuteTool(plugin: any) {
4
- return {
5
- scope: 'CUSTOM',
6
- execution: 'backend',
7
- defaultPermission: 'ASK',
8
-
9
- introduction: {
10
- title: 'Skill Hub - Universal Skill Executor',
11
- about:
12
- 'A universal gateway to execute predefined specialized skills (data processing, complex calculations, file generation, etc.) inside a secure Python/Node.js sandbox.',
13
- },
14
-
15
- definition: {
16
- name: 'skill_hub_execute',
17
- description: `A universal gateway to execute various predefined specialized skills (e.g., data transformation, calculations, document/presentation generation) in an isolated sandbox.
18
- HOW TO USE THIS TOOL:
19
- 1. If you don't know the exact 'skillName' or its required 'input' schema, first call this tool with { "action": "list" } to discover all available skills.
20
- 2. For complex workflow skills, call { "action": "describe", "skillName": "<exact_skill_name>" } to load the full instructions before execution.
21
- 3. To run a skill, call this tool with { "action": "execute", "skillName": "<exact_skill_name>", "input": { <parameters> } }.
22
- CRITICAL: Do NOT guess or hallucinate the 'input' object. You MUST strictly provide the parameters matching the JSON schema defined for that specific skill.
23
- The skill's output may contain text results, structured JSON data, or download URLs for generated files.
24
- IMPORTANT: If the skill returns file download URLs, you MUST format them as clickable Markdown links (e.g., [Download filename.ext](/api/attachments/...)) in your final response to the user.`,
25
- schema: {
26
- type: 'object',
27
- properties: {
28
- action: {
29
- type: 'string',
30
- enum: ['list', 'describe', 'execute'],
31
- description: '"list" to see available skills, "describe" to load full instructions, "execute" to run one',
32
- },
33
- skillName: {
34
- type: 'string',
35
- description: 'Skill name (required for "execute" action)',
36
- },
37
- input: {
38
- type: 'object',
39
- description: 'Input parameters matching the skill inputSchema',
40
- },
41
- },
42
- required: ['action'],
43
- },
44
- },
45
-
46
- async invoke(ctx: any, args: Record<string, any>, _id?: string) {
47
- plugin.app.logger.info(`[skill-execute] Tool invoked with action: ${args.action}, skillName: ${args.skillName}`);
48
-
49
- // Action: list available skills
50
- if (args.action === 'list') {
51
- const skills = await plugin.db.getRepository('skillDefinitions').find({
52
- filter: { enabled: true },
53
- fields: ['name', 'title', 'description', 'language', 'inputSchema', 'instructions', 'storageType'],
54
- });
55
-
56
- const skillList = skills.map((s: any) => ({
57
- name: s.get('name'),
58
- title: s.get('title'),
59
- description: s.get('description'),
60
- language: s.get('language'),
61
- inputSchema: parseJsonText(s.get('inputSchema'), null),
62
- hasInstructions: !!s.get('instructions') || s.get('storageType') === 'plugin',
63
- storageType: s.get('storageType'),
64
- }));
65
-
66
- return {
67
- status: 'success',
68
- content: JSON.stringify({ skills: skillList }),
69
- };
70
- }
71
-
72
- // Action: describe one skill with full workflow instructions
73
- if (args.action === 'describe') {
74
- if (!args.skillName) {
75
- return { status: 'error', content: 'Missing skillName parameter' };
76
- }
77
-
78
- const skill = await plugin.db.getRepository('skillDefinitions').findOne({
79
- filter: { name: args.skillName, enabled: true },
80
- });
81
-
82
- if (!skill) {
83
- return {
84
- status: 'error',
85
- content: `Skill "${args.skillName}" not found or disabled`,
86
- };
87
- }
88
-
89
- const instructions =
90
- typeof plugin.getSkillInstructions === 'function'
91
- ? await plugin.getSkillInstructions(skill)
92
- : skill.get('instructions');
93
-
94
- return {
95
- status: 'success',
96
- content: JSON.stringify({
97
- name: skill.get('name'),
98
- title: skill.get('title'),
99
- description: skill.get('description'),
100
- language: skill.get('language'),
101
- inputSchema: parseJsonText(skill.get('inputSchema'), null),
102
- packages: parseJsonText(skill.get('packages'), []),
103
- storageType: skill.get('storageType'),
104
- storageUrl: skill.get('storageUrl'),
105
- instructions,
106
- }),
107
- };
108
- }
109
-
110
- // Action: execute skill
111
- if (args.action === 'execute') {
112
- if (!args.skillName) {
113
- return { status: 'error', content: 'Missing skillName parameter' };
114
- }
115
-
116
- const skill = await plugin.db.getRepository('skillDefinitions').findOne({
117
- filter: { name: args.skillName, enabled: true },
118
- });
119
-
120
- if (!skill) {
121
- return {
122
- status: 'error',
123
- content: `Skill "${args.skillName}" not found or disabled`,
124
- };
125
- }
126
-
127
- try {
128
- const result = await plugin.executeSkill(skill, args.input || {}, ctx);
129
-
130
- return {
131
- status: result.status === 'succeeded' ? 'success' : 'error',
132
- content: JSON.stringify({
133
- message:
134
- result.status === 'succeeded'
135
- ? `Executed successfully. ${result.files?.length || 0} file(s) generated.`
136
- : `Failed: ${result.stderr}`,
137
- stdout: result.stdout,
138
- stderr: result.stderr,
139
- files: result.files,
140
- execId: result.execId,
141
- agentLoopRunId: result.agentLoopRunId,
142
- agentLoopStepId: result.agentLoopStepId,
143
- durationMs: result.durationMs,
144
- }),
145
- };
146
- } catch (error) {
147
- return {
148
- status: 'error',
149
- content: `Execution error: ${error instanceof Error ? error.message : String(error)}`,
150
- };
151
- }
152
- }
153
-
154
- return {
155
- status: 'error',
156
- content: `Unknown action "${args.action}". Use "list", "describe", or "execute".`,
157
- };
158
- },
159
- };
160
- }
1
+ import { parseJsonText } from '../skill-hub/utils/json-fields';
2
+ import type { ToolsRuntime } from '@nocobase/ai';
3
+
4
+ type ToolRuntimeInput = string | ToolsRuntime | undefined;
5
+
6
+ function normalizeRuntime(runtime: ToolRuntimeInput): ToolsRuntime | undefined {
7
+ if (!runtime) return undefined;
8
+ if (typeof runtime === 'string') {
9
+ return { toolCallId: runtime, writer: () => {} };
10
+ }
11
+ return runtime;
12
+ }
13
+
14
+ export function createSkillExecuteTool(plugin: any) {
15
+ return {
16
+ scope: 'CUSTOM',
17
+ execution: 'backend',
18
+ // Intentionally fixed to ASK. This is the universal gateway: a single tool
19
+ // whose `execute` action can run ANY enabled skill by name, so the
20
+ // per-skill `autoCall` flag cannot be honored here the harness decides
21
+ // approval from `defaultPermission` before invoke, when the target skill is
22
+ // not yet known. The per-skill dynamic tools (`skill_hub_<name>`) are the
23
+ // fast path that respect `autoCall: true ALLOW`. Keeping the generic
24
+ // gateway on ASK is deliberate defense-in-depth, not an oversight.
25
+ defaultPermission: 'ASK',
26
+
27
+ introduction: {
28
+ title: 'Skill Hub - Universal Skill Executor',
29
+ about:
30
+ 'A universal gateway to execute predefined specialized skills (data processing, complex calculations, file generation, etc.) inside a secure Python/Node.js sandbox.',
31
+ },
32
+
33
+ definition: {
34
+ name: 'skill_hub_execute',
35
+ description: `A universal gateway to execute various predefined specialized skills (e.g., data transformation, calculations, document/presentation generation) in an isolated sandbox.
36
+ HOW TO USE THIS TOOL:
37
+ 1. If you don't know the exact 'skillName' or its required 'input' schema, first call this tool with { "action": "list" } to discover all available skills.
38
+ 2. For complex workflow skills, call { "action": "describe", "skillName": "<exact_skill_name>" } to load the full instructions before execution.
39
+ 3. To run a skill, call this tool with { "action": "execute", "skillName": "<exact_skill_name>", "input": { <parameters> } }.
40
+ CRITICAL: Do NOT guess or hallucinate the 'input' object. You MUST strictly provide the parameters matching the JSON schema defined for that specific skill.
41
+ The skill's output may contain text results, structured JSON data, or download URLs for generated files.
42
+ IMPORTANT: If the skill returns file download URLs, you MUST format them as clickable Markdown links (e.g., [Download filename.ext](/api/attachments/...)) in your final response to the user.`,
43
+ schema: {
44
+ type: 'object',
45
+ properties: {
46
+ action: {
47
+ type: 'string',
48
+ enum: ['list', 'describe', 'execute'],
49
+ description: '"list" to see available skills, "describe" to load full instructions, "execute" to run one',
50
+ },
51
+ skillName: {
52
+ type: 'string',
53
+ description: 'Skill name (required for "execute" action)',
54
+ },
55
+ input: {
56
+ type: 'object',
57
+ description: 'Input parameters matching the skill inputSchema',
58
+ },
59
+ },
60
+ required: ['action'],
61
+ },
62
+ },
63
+
64
+ async invoke(ctx: any, args: Record<string, any>, runtime?: ToolRuntimeInput) {
65
+ plugin.app.logger.info(`[skill-execute] Tool invoked with action: ${args.action}, skillName: ${args.skillName}`);
66
+
67
+ // Action: list available skills
68
+ if (args.action === 'list') {
69
+ const skills = await plugin.db.getRepository('skillDefinitions').find({
70
+ filter: { enabled: true },
71
+ fields: ['name', 'title', 'description', 'language', 'inputSchema', 'instructions', 'storageType'],
72
+ });
73
+
74
+ const skillList = skills.map((s: any) => ({
75
+ name: s.get('name'),
76
+ title: s.get('title'),
77
+ description: s.get('description'),
78
+ language: s.get('language'),
79
+ inputSchema: parseJsonText(s.get('inputSchema'), null),
80
+ hasInstructions: !!s.get('instructions') || s.get('storageType') === 'plugin',
81
+ storageType: s.get('storageType'),
82
+ }));
83
+
84
+ return {
85
+ status: 'success',
86
+ content: JSON.stringify({ skills: skillList }),
87
+ };
88
+ }
89
+
90
+ // Action: describe one skill with full workflow instructions
91
+ if (args.action === 'describe') {
92
+ if (!args.skillName) {
93
+ return { status: 'error', content: 'Missing skillName parameter' };
94
+ }
95
+
96
+ const skill = await plugin.db.getRepository('skillDefinitions').findOne({
97
+ filter: { name: args.skillName, enabled: true },
98
+ });
99
+
100
+ if (!skill) {
101
+ return {
102
+ status: 'error',
103
+ content: `Skill "${args.skillName}" not found or disabled`,
104
+ };
105
+ }
106
+
107
+ const instructions =
108
+ typeof plugin.getSkillInstructions === 'function'
109
+ ? await plugin.getSkillInstructions(skill)
110
+ : skill.get('instructions');
111
+
112
+ return {
113
+ status: 'success',
114
+ content: JSON.stringify({
115
+ name: skill.get('name'),
116
+ title: skill.get('title'),
117
+ description: skill.get('description'),
118
+ language: skill.get('language'),
119
+ inputSchema: parseJsonText(skill.get('inputSchema'), null),
120
+ packages: parseJsonText(skill.get('packages'), []),
121
+ storageType: skill.get('storageType'),
122
+ storageUrl: skill.get('storageUrl'),
123
+ instructions,
124
+ }),
125
+ };
126
+ }
127
+
128
+ // Action: execute skill
129
+ if (args.action === 'execute') {
130
+ if (!args.skillName) {
131
+ return { status: 'error', content: 'Missing skillName parameter' };
132
+ }
133
+
134
+ const skill = await plugin.db.getRepository('skillDefinitions').findOne({
135
+ filter: { name: args.skillName, enabled: true },
136
+ });
137
+
138
+ if (!skill) {
139
+ return {
140
+ status: 'error',
141
+ content: `Skill "${args.skillName}" not found or disabled`,
142
+ };
143
+ }
144
+
145
+ try {
146
+ const normalizedRuntime = normalizeRuntime(runtime);
147
+ const previousRuntime = ctx.runtime;
148
+ if (normalizedRuntime) {
149
+ ctx.runtime = normalizedRuntime;
150
+ }
151
+ let result;
152
+ try {
153
+ result = await plugin.executeSkill(skill, args.input || {}, ctx);
154
+ } finally {
155
+ if (normalizedRuntime) {
156
+ if (previousRuntime === undefined) {
157
+ delete ctx.runtime;
158
+ } else {
159
+ ctx.runtime = previousRuntime;
160
+ }
161
+ }
162
+ }
163
+
164
+ return {
165
+ status: result.status === 'succeeded' ? 'success' : 'error',
166
+ content: JSON.stringify({
167
+ message:
168
+ result.status === 'succeeded'
169
+ ? `Executed successfully. ${result.files?.length || 0} file(s) generated.`
170
+ : `Failed: ${result.stderr}`,
171
+ stdout: result.stdout,
172
+ stderr: result.stderr,
173
+ files: result.files,
174
+ execId: result.execId,
175
+ agentLoopRunId: result.agentLoopRunId,
176
+ agentLoopStepId: result.agentLoopStepId,
177
+ durationMs: result.durationMs,
178
+ }),
179
+ };
180
+ } catch (error) {
181
+ return {
182
+ status: 'error',
183
+ content: `Execution error: ${error instanceof Error ? error.message : String(error)}`,
184
+ };
185
+ }
186
+ }
187
+
188
+ return {
189
+ status: 'error',
190
+ content: `Unknown action "${args.action}". Use "list", "describe", or "execute".`,
191
+ };
192
+ },
193
+ };
194
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Single accessor for the core AI tools manager.
3
+ *
4
+ * plugin-ai exposes the tools manager at `app.aiManager.toolsManager`, and the
5
+ * same instance is reachable via `pluginAI.ai.toolsManager`. Mixing the two
6
+ * access paths risks registering tools on one object while resolving them on
7
+ * another if the wiring ever diverges. Every orchestrator call site goes
8
+ * through this helper so registration and resolution always share one manager.
9
+ */
10
+ export function getAIToolsManager(app: any) {
11
+ const toolsManager = app?.aiManager?.toolsManager;
12
+ if (!toolsManager) {
13
+ throw new Error(
14
+ '[AgentOrchestrator] app.aiManager.toolsManager is not available. ' +
15
+ 'Ensure @nocobase/plugin-ai is enabled and loaded before plugin-agent-orchestrator.',
16
+ );
17
+ }
18
+ return toolsManager;
19
+ }
20
+
21
+ /** Same accessor, but returns undefined instead of throwing (best-effort call sites). */
22
+ export function tryGetAIToolsManager(app: any) {
23
+ return app?.aiManager?.toolsManager;
24
+ }
@@ -44,6 +44,20 @@ export function currentUserId(ctx: any) {
44
44
  return ctx?.state?.currentUser?.id || ctx?.auth?.user?.id;
45
45
  }
46
46
 
47
+ /**
48
+ * Whether the current request is made by an admin/root user.
49
+ * Mirrors the owner/admin check used by skillHub:download so list/get
50
+ * scoping and file access stay consistent.
51
+ */
52
+ export function isAdminUser(ctx: any): boolean {
53
+ const roles = ctx?.state?.currentUser?.roles || ctx?.auth?.user?.roles;
54
+ if (!Array.isArray(roles)) return false;
55
+ return roles.some((r: any) => {
56
+ const name = typeof r === 'string' ? r : r?.name;
57
+ return name === 'root' || name === 'admin';
58
+ });
59
+ }
60
+
47
61
  /** Get action params values from ctx. */
48
62
  export function valuesFromCtx(ctx: any) {
49
63
  return ctx?.action?.params?.values || ctx?.request?.body || {};
@@ -0,0 +1,116 @@
1
+ export type EmployeeToolBinding = {
2
+ name: string;
3
+ autoCall?: boolean;
4
+ };
5
+
6
+ type RecordLike = Record<string, unknown>;
7
+
8
+ function isRecord(value: unknown): value is RecordLike {
9
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
10
+ }
11
+
12
+ export function isOrchestratorToolName(name: string) {
13
+ return (
14
+ name === 'orchestrator_plan_goal' ||
15
+ name === 'orchestrator_execute_plan' ||
16
+ name === 'orchestrator_status' ||
17
+ name === 'orchestrator_cancel' ||
18
+ name === 'external_rag_search' ||
19
+ name === 'skill_hub_execute' ||
20
+ name.startsWith('delegate_') ||
21
+ name.startsWith('dispatch_subagents_') ||
22
+ name.startsWith('skill_hub_') ||
23
+ name.startsWith('browser_') ||
24
+ name.startsWith('drawio-')
25
+ );
26
+ }
27
+
28
+ function toToolBinding(value: unknown): EmployeeToolBinding | null {
29
+ if (typeof value === 'string') {
30
+ const name = value.trim();
31
+ return name ? { name, autoCall: false } : null;
32
+ }
33
+ if (!isRecord(value) || typeof value.name !== 'string') {
34
+ return null;
35
+ }
36
+ const name = value.name.trim();
37
+ if (!name) {
38
+ return null;
39
+ }
40
+ return {
41
+ name,
42
+ autoCall: value.autoCall === true,
43
+ };
44
+ }
45
+
46
+ function addToolBinding(toolsByName: Map<string, EmployeeToolBinding>, binding: EmployeeToolBinding | null) {
47
+ if (!binding) return;
48
+ if (!toolsByName.has(binding.name)) {
49
+ toolsByName.set(binding.name, binding);
50
+ }
51
+ }
52
+
53
+ export function normalizeAIEmployeeSkillSettings(value: unknown): {
54
+ changed: boolean;
55
+ skillSettings: RecordLike & { skills: string[]; tools: EmployeeToolBinding[] };
56
+ } {
57
+ const source = isRecord(value) ? value : {};
58
+ const toolsByName = new Map<string, EmployeeToolBinding>();
59
+ const nextSkills: string[] = [];
60
+ let changed = false;
61
+
62
+ const sourceTools = source.tools;
63
+ if (Array.isArray(sourceTools)) {
64
+ for (const item of sourceTools) {
65
+ const binding = toToolBinding(item);
66
+ if (binding) {
67
+ addToolBinding(toolsByName, binding);
68
+ if (typeof item === 'string') {
69
+ changed = true;
70
+ }
71
+ } else {
72
+ changed = true;
73
+ }
74
+ }
75
+ }
76
+
77
+ const sourceSkills = source.skills;
78
+ if (Array.isArray(sourceSkills)) {
79
+ for (const item of sourceSkills) {
80
+ if (typeof item === 'string') {
81
+ const name = item.trim();
82
+ if (!name) {
83
+ changed = true;
84
+ continue;
85
+ }
86
+ if (isOrchestratorToolName(name)) {
87
+ addToolBinding(toolsByName, { name, autoCall: false });
88
+ changed = true;
89
+ continue;
90
+ }
91
+ nextSkills.push(name);
92
+ if (name !== item) {
93
+ changed = true;
94
+ }
95
+ continue;
96
+ }
97
+
98
+ const legacyToolBinding = toToolBinding(item);
99
+ if (legacyToolBinding) {
100
+ addToolBinding(toolsByName, legacyToolBinding);
101
+ }
102
+ changed = true;
103
+ }
104
+ }
105
+
106
+ const normalized = {
107
+ ...source,
108
+ skills: nextSkills,
109
+ tools: Array.from(toolsByName.values()),
110
+ };
111
+
112
+ return {
113
+ changed,
114
+ skillSettings: normalized,
115
+ };
116
+ }