plugin-agent-orchestrator 1.0.28 → 1.0.32

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 (108) hide show
  1. package/README.md +9 -7
  2. package/dist/client/index.js +1 -1
  3. package/dist/client-v2/{214.723affb37c13bf7a.js → 214.79650a549273f163.js} +1 -1
  4. package/dist/client-v2/264.718a107e43fc163c.js +10 -0
  5. package/dist/client-v2/373.f5d5292e53c4e832.js +10 -0
  6. package/dist/client-v2/{41.1805b2edfaa4afe2.js → 41.ba6e080cc0488143.js} +1 -1
  7. package/dist/client-v2/418.29e713f79131eece.js +10 -0
  8. package/dist/client-v2/619.bd3c5698b40705c3.js +10 -0
  9. package/dist/client-v2/677.a991ce0250ff5c77.js +10 -0
  10. package/dist/client-v2/{70.a15d7fcec7c41768.js → 70.bda9518881c05360.js} +1 -1
  11. package/dist/client-v2/925.f5370de8f6632d65.js +10 -0
  12. package/dist/client-v2/index.js +1 -1
  13. package/dist/externalVersion.js +7 -10
  14. package/dist/locale/en-US.json +94 -25
  15. package/dist/locale/vi-VN.json +94 -25
  16. package/dist/locale/zh-CN.json +94 -25
  17. package/dist/server/collections/agent-execution-spans.js +37 -0
  18. package/dist/server/collections/agent-harness-profiles.js +2 -2
  19. package/dist/server/collections/agent-memory-contexts.js +125 -0
  20. package/dist/server/collections/orchestrator-logs.js +2 -2
  21. package/dist/server/migrations/20260425000000-add-interaction-schema.js +3 -1
  22. package/dist/server/migrations/20260427000000-change-packages-to-text.js +3 -1
  23. package/dist/server/migrations/20260427000001-change-other-json-to-text.js +6 -2
  24. package/dist/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.js +21 -19
  25. package/dist/server/migrations/20260621000000-native-policy-profile-defaults.js +193 -0
  26. package/dist/server/plugin.js +128 -74
  27. package/dist/server/resources/agent-monitor.js +454 -0
  28. package/dist/server/services/AgentHarness.js +24 -499
  29. package/dist/server/services/AgentMemoryContextService.js +216 -0
  30. package/dist/server/services/ExecutionSpanService.js +2 -2
  31. package/dist/server/services/NativeSubAgentObserver.js +413 -0
  32. package/dist/server/skill-hub/plugin.js +81 -5
  33. package/dist/server/skill-hub/tasks/SkillExecutionTask.js +9 -3
  34. package/dist/server/tools/delegate-task.js +11 -589
  35. package/dist/server/utils/skill-settings.js +18 -1
  36. package/package.json +47 -49
  37. package/src/client/AIEmployeesContext.tsx +5 -18
  38. package/src/client/AgentRunsTab.tsx +2 -771
  39. package/src/client/HarnessProfilesTab.tsx +2 -257
  40. package/src/client/OrchestratorSettings.tsx +97 -106
  41. package/src/client/RulesTab.tsx +2 -788
  42. package/src/client/plugin.tsx +0 -2
  43. package/src/client/skill-hub/components/ExecutionHistory.tsx +200 -202
  44. package/src/client/skill-hub/components/ExecutionProgress.tsx +51 -55
  45. package/src/client/skill-hub/components/LoopSettings.tsx +331 -331
  46. package/src/client/skill-hub/components/SkillEditor.tsx +43 -39
  47. package/src/client/skill-hub/components/SkillManager.tsx +194 -181
  48. package/src/client/skill-hub/components/SkillTestPanel.tsx +141 -145
  49. package/src/client/skill-hub/locale.ts +16 -16
  50. package/src/client/skill-hub/tools/SkillHubCard.tsx +104 -109
  51. package/src/client/skill-hub/tools/loopTemplates.ts +52 -52
  52. package/src/client/skill-hub/utils/jsonFields.ts +7 -3
  53. package/src/client-v2/components/AIEmployeesContext.tsx +3 -16
  54. package/src/client-v2/components/AgentRunsTab.tsx +182 -455
  55. package/src/client-v2/components/HarnessProfilesTab.tsx +34 -31
  56. package/src/client-v2/components/RulesTab.tsx +2 -782
  57. package/src/client-v2/components/TracingTab.tsx +1 -1
  58. package/src/client-v2/hooks/useApiRequest.ts +8 -1
  59. package/src/client-v2/pages/RulesPage.tsx +2 -2
  60. package/src/client-v2/plugin.tsx +3 -3
  61. package/src/locale/en-US.json +94 -25
  62. package/src/locale/vi-VN.json +94 -25
  63. package/src/locale/zh-CN.json +94 -25
  64. package/src/server/__tests__/native-sub-agent-observer.test.ts +246 -0
  65. package/src/server/__tests__/skill-settings.test.ts +6 -6
  66. package/src/server/__tests__/smoke.test.ts +1 -0
  67. package/src/server/collections/agent-execution-spans.ts +37 -0
  68. package/src/server/collections/agent-harness-profiles.ts +59 -59
  69. package/src/server/collections/agent-loop-events.ts +71 -71
  70. package/src/server/collections/agent-loop-steps.ts +144 -144
  71. package/src/server/collections/agent-memory-contexts.ts +95 -0
  72. package/src/server/collections/orchestrator-logs.ts +4 -4
  73. package/src/server/collections/skill-definitions.ts +111 -111
  74. package/src/server/collections/skill-executions.ts +106 -106
  75. package/src/server/collections/skill-loop-configs.ts +65 -65
  76. package/src/server/migrations/20260423000000-add-progress-fields.ts +14 -14
  77. package/src/server/migrations/20260425000000-add-interaction-schema.ts +3 -1
  78. package/src/server/migrations/20260427000000-change-packages-to-text.ts +4 -2
  79. package/src/server/migrations/20260427000001-change-other-json-to-text.ts +9 -5
  80. package/src/server/migrations/20260524000000-add-agent-loop-fields-to-skill-executions.ts +30 -30
  81. package/src/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.ts +145 -142
  82. package/src/server/migrations/20260615000000-normalize-ai-employee-tool-bindings.ts +2 -2
  83. package/src/server/migrations/20260621000000-native-policy-profile-defaults.ts +193 -0
  84. package/src/server/plugin.ts +151 -94
  85. package/src/server/resources/agent-monitor.ts +482 -0
  86. package/src/server/services/AgentHarness.ts +38 -623
  87. package/src/server/services/AgentMemoryContextService.ts +256 -0
  88. package/src/server/services/AgentPlanValidator.ts +73 -73
  89. package/src/server/services/ExecutionSpanService.ts +6 -2
  90. package/src/server/services/FileManager.ts +144 -144
  91. package/src/server/services/NativeSubAgentObserver.ts +507 -0
  92. package/src/server/services/SkillManager.ts +583 -583
  93. package/src/server/services/SkillRepositoryService.ts +5 -7
  94. package/src/server/services/TokenTracker.ts +3 -3
  95. package/src/server/services/WorkerEnvManager.ts +1 -2
  96. package/src/server/skill-hub/actions/git-import.ts +5 -7
  97. package/src/server/skill-hub/plugin.ts +89 -6
  98. package/src/server/skill-hub/tasks/SkillExecutionTask.ts +470 -460
  99. package/src/server/skill-hub/utils/json-fields.ts +1 -1
  100. package/src/server/tools/delegate-task.ts +13 -847
  101. package/src/server/utils/skill-settings.ts +24 -6
  102. package/dist/client-v2/264.0533912e6c5ea2d7.js +0 -10
  103. package/dist/client-v2/418.5ae055abf141820e.js +0 -10
  104. package/dist/client-v2/619.d99d3c9e61c99064.js +0 -10
  105. package/dist/client-v2/892.72db4161511c8a16.js +0 -10
  106. package/dist/client-v2/926.87f660b670d85bcc.js +0 -10
  107. package/src/client/tools/PlanApprovalCard.tsx +0 -176
  108. package/src/client/tools/registerOrchestratorCards.ts +0 -17
@@ -1,864 +1,30 @@
1
- import { z } from 'zod';
2
- import { createHash } from 'crypto';
3
- import { Context } from '@nocobase/actions';
4
- // @ts-ignore - subpath export types resolve at build time via NocoBase bundler
5
- import { createReactAgent } from '@langchain/langgraph/prebuilt';
6
- import { DynamicStructuredTool } from '@langchain/core/tools';
7
- import { HumanMessage, SystemMessage } from '@langchain/core/messages';
8
- import type PluginAIServer from '@nocobase/plugin-ai/dist/server';
9
- import type { ToolsEntry, ToolsRuntime } from '@nocobase/ai';
10
- import {
11
- ExecutionSpanService,
12
- getOrchestratorTraceContext,
13
- setOrchestratorTraceContext,
14
- } from '../services/ExecutionSpanService';
15
- import { captureCtxSnapshot, normalizeEmployeeUsername, trimText as truncateText, nowIso } from '../utils/ctx-utils';
16
- import { logDelegation as sharedLogDelegation } from '../utils/logging';
17
- import { CtxSnapshot, TraceEvent } from '../types';
18
-
19
- /**
20
- * Maximum delegation depth key stored in ctx metadata.
21
- * Used to prevent circular/recursive delegation chains.
22
- */
23
- const ORCHESTRATOR_DEPTH_KEY = '__orchestratorDepth';
24
- /**
25
- * Context key for tracking the full delegation path.
26
- * Used to detect and prevent circular delegation chains.
27
- */
28
- const ORCHESTRATOR_PATH_KEY = '__orchestratorPath';
29
-
30
- /** Max sub-agents that the dispatch tool runs concurrently in one call. */
31
- const MAX_DISPATCH_CONCURRENCY = 5;
32
- /** Hard cap on tasks per dispatch call to keep output bounded. */
33
- const MAX_DISPATCH_TASKS = 20;
34
- /** OpenAI/Anthropic tool-name limit. Names exceeding this are silently rejected by providers. */
35
- const MAX_TOOL_NAME_LENGTH = 64;
36
-
37
- type AgentExecutionResult = {
38
- content: string;
39
- messages: any[];
40
- };
41
-
42
- type ToolRuntimeInput = string | ToolsRuntime | undefined;
43
-
44
- function getToolCallId(runtime: ToolRuntimeInput) {
45
- return typeof runtime === 'string' ? runtime : runtime?.toolCallId;
46
- }
47
-
48
1
  function sanitizeToolPart(value: string) {
49
2
  return (value || '').replace(/[^a-zA-Z0-9_-]/g, '_');
50
3
  }
51
4
 
52
- function buildDelegateToolName(leaderUsername: string, subAgentUsername: string) {
5
+ export function buildDelegateToolName(leaderUsername: string, subAgentUsername: string) {
53
6
  return `delegate_${sanitizeToolPart(leaderUsername)}_to_${sanitizeToolPart(subAgentUsername)}`;
54
7
  }
55
8
 
56
- function buildDispatchToolName(leaderUsername: string) {
9
+ export function buildDispatchToolName(leaderUsername: string) {
57
10
  return `dispatch_subagents_${sanitizeToolPart(leaderUsername)}`;
58
11
  }
59
12
 
60
- function createRootRunId(seed = '') {
61
- const hash = createHash('sha1').update(`${Date.now()}::${Math.random()}::${seed}`).digest('hex').slice(0, 10);
62
- return `run_${Date.now()}_${hash}`;
63
- }
64
-
65
- /**
66
- * Set of tool names this plugin actually registered in the most recent build.
67
- * Sub-agents must not see these tools (would enable circular delegation), but
68
- * we don't want to drop unrelated user tools whose names happen to start with
69
- * "delegate_" — so we filter by the known registry, not a regex pattern.
70
- */
71
- let registeredDelegateNamesByPlugin: WeakMap<object, ReadonlySet<string>> = new WeakMap();
72
-
73
- function isDelegateToolName(plugin: any, toolName: string) {
74
- return registeredDelegateNamesByPlugin.get(plugin)?.has(toolName) ?? false;
75
- }
76
-
77
- /**
78
- * Run async work over `items` with at most `limit` concurrent executions.
79
- * Preserves input order in the returned array.
80
- */
81
- async function runWithConcurrency<T, R>(
82
- items: T[],
83
- limit: number,
84
- fn: (item: T, index: number) => Promise<R>,
85
- ): Promise<R[]> {
86
- const results: R[] = new Array(items.length);
87
- let cursor = 0;
88
- const workerCount = Math.max(1, Math.min(limit, items.length));
89
- const workers = Array.from({ length: workerCount }, async () => {
90
- while (cursor < items.length) {
91
- const i = cursor;
92
- cursor += 1;
93
- results[i] = await fn(items[i], i);
94
- }
95
- });
96
- await Promise.all(workers);
97
- return results;
98
- }
99
-
100
- function createDelegateToolOptions(
101
- plugin: any,
102
- options: {
103
- leaderUsername: string;
104
- subAgentUsername: string;
105
- subAgentEmployee: any;
106
- maxDepth?: number;
107
- timeout?: number;
108
- toolName: string;
109
- legacyAlias?: boolean;
110
- llmService?: string;
111
- model?: string;
112
- recursionLimit?: number;
113
- },
114
- ) {
115
- const {
116
- leaderUsername,
117
- subAgentUsername,
118
- subAgentEmployee,
119
- maxDepth,
120
- timeout,
121
- toolName,
122
- legacyAlias,
123
- llmService,
124
- model,
125
- recursionLimit,
126
- } = options;
127
- const dispatchToolName = buildDispatchToolName(leaderUsername);
128
- const toolDescription = [
129
- `Delegate a task from "${leaderUsername}" to the AI Employee "${subAgentEmployee.nickname || subAgentUsername}".`,
130
- legacyAlias ? 'This is a backward-compatible alias for existing skill assignments.' : '',
131
- subAgentEmployee.about ? `Specialist profile: ${subAgentEmployee.about.substring(0, 200)}` : '',
132
- 'The sub-agent will execute the task independently and return its final answer.',
133
- `For multiple INDEPENDENT sub-tasks, prefer "${dispatchToolName}" to fan-out in one call (up to ${MAX_DISPATCH_CONCURRENCY} run in parallel), or emit several delegate_* calls in the SAME assistant turn so they run concurrently.`,
134
- ]
135
- .filter(Boolean)
136
- .join(' ');
137
-
138
- return {
139
- scope: 'CUSTOM',
140
- execution: 'backend',
141
- defaultPermission: 'ASK',
142
- silence: false,
143
- introduction: {
144
- title: `[${leaderUsername}] ${subAgentEmployee.nickname || subAgentUsername}${legacyAlias ? ' (legacy)' : ''}`,
145
- about: toolDescription,
146
- },
147
- definition: {
148
- name: toolName,
149
- description: toolDescription,
150
- schema: z.object({
151
- task: z.string().describe('The detailed task description for the sub-agent to execute.'),
152
- context: z
153
- .string()
154
- .optional()
155
- .describe('Optional additional context to help the sub-agent understand the task better.'),
156
- }),
157
- },
158
- invoke: async (ctx: Context, args: { task: string; context?: string }, runtime?: ToolRuntimeInput) => {
159
- const id = getToolCallId(runtime) || `delegate-${Date.now()}`;
160
- const callingEmployee = await resolveCallingEmployee(ctx, plugin);
161
- if (!callingEmployee) {
162
- await logDelegation(ctx, plugin, {
163
- leaderUsername,
164
- subAgentUsername,
165
- toolName,
166
- task: args.task,
167
- context: args.context,
168
- result: '',
169
- status: 'error',
170
- depth: (ctx as any)[ORCHESTRATOR_DEPTH_KEY] ?? 0,
171
- durationMs: 0,
172
- error: `Cannot determine calling AI employee for delegation tool "${toolName}".`,
173
- });
174
- return {
175
- status: 'error' as const,
176
- content: `Cannot determine calling AI employee for "${toolName}". Start the request from an AI Employee conversation so leader scoping can be enforced.`,
177
- };
178
- }
179
- if (callingEmployee && callingEmployee !== leaderUsername) {
180
- await logDelegation(ctx, plugin, {
181
- leaderUsername,
182
- subAgentUsername,
183
- toolName,
184
- task: args.task,
185
- context: args.context,
186
- result: '',
187
- status: 'error',
188
- depth: (ctx as any)[ORCHESTRATOR_DEPTH_KEY] ?? 0,
189
- durationMs: 0,
190
- error: `Employee "${callingEmployee}" is not authorized to use this delegation rule.`,
191
- });
192
- return {
193
- status: 'error' as const,
194
- content: `Employee "${callingEmployee}" is not authorized to delegate to "${subAgentUsername}". Configure an orchestration rule first.`,
195
- };
196
- }
197
-
198
- return invokeDelegateTask(ctx, plugin, {
199
- leaderUsername,
200
- subAgentUsername,
201
- subAgentEmployee,
202
- task: args.task,
203
- context: args.context,
204
- maxDepth: maxDepth ?? 1,
205
- timeout: timeout ?? 120000,
206
- toolCallId: id,
207
- toolName,
208
- llmService,
209
- model,
210
- recursionLimit,
211
- });
212
- },
213
- };
214
- }
215
-
216
- type DispatchRuleEntry = {
217
- rule: any;
218
- employee: any;
219
- };
220
-
221
- type DispatchTaskResult = {
222
- index: number;
223
- subAgent: string;
224
- status: 'success' | 'error';
225
- content: string;
226
- durationMs: number;
227
- };
228
-
229
- function formatDispatchResults(results: DispatchTaskResult[], rulesBySubAgent: Map<string, DispatchRuleEntry>) {
230
- const total = results.length;
231
- const ok = results.filter((r) => r.status === 'success').length;
232
- const lines = [
233
- `Dispatched ${total} sub-task(s) — ${ok} succeeded, ${
234
- total - ok
235
- } failed (max ${MAX_DISPATCH_CONCURRENCY} ran in parallel).`,
236
- '',
237
- ];
238
- for (const r of results) {
239
- const employee = rulesBySubAgent.get(r.subAgent)?.employee;
240
- const displayName = employee?.nickname || r.subAgent;
241
- const dur = `${(r.durationMs / 1000).toFixed(1)}s`;
242
- lines.push(`--- [${r.index + 1}] ${displayName} (${r.subAgent}) [${r.status}] (${dur}) ---`);
243
- lines.push(r.content || '(empty)');
244
- lines.push('');
245
- }
246
- return lines.join('\n').trimEnd();
247
- }
248
-
249
- /**
250
- * Build a single fan-out tool per leader. The leader passes a list of
251
- * `{ subAgent, task, context? }` items; we run them concurrently (capped at
252
- * MAX_DISPATCH_CONCURRENCY) and aggregate the results into one response.
253
- *
254
- * Each underlying execution still goes through `invokeDelegateTask`, so depth
255
- * limits, per-rule timeouts, LLM overrides, and orchestratorLogs entries
256
- * behave identically to a direct `delegate_*_to_*` call.
257
- */
258
- function createDispatchToolOptions(
259
- plugin: any,
260
- options: {
261
- leaderUsername: string;
262
- rulesBySubAgent: Map<string, DispatchRuleEntry>;
263
- },
264
- ) {
265
- const { leaderUsername, rulesBySubAgent } = options;
266
- const toolName = buildDispatchToolName(leaderUsername);
267
- const subAgentNames = Array.from(rulesBySubAgent.keys());
268
-
269
- const subAgentList = subAgentNames
270
- .map((username) => {
271
- const entry = rulesBySubAgent.get(username);
272
- if (!entry) return `- ${username}`;
273
- const profile = entry.employee?.about ? ` — ${String(entry.employee.about).substring(0, 120)}` : '';
274
- const display = entry.employee?.nickname ? ` (${entry.employee.nickname})` : '';
275
- return `- ${username}${display}${profile}`;
276
- })
277
- .join('\n');
278
-
279
- const description = [
280
- `Dispatch multiple tasks from "${leaderUsername}" to its configured sub-agents in one call.`,
281
- `At most ${MAX_DISPATCH_CONCURRENCY} sub-tasks run in parallel; up to ${MAX_DISPATCH_TASKS} tasks per call.`,
282
- 'Use this when you have already planned independent sub-tasks and want to fan-out, then aggregate the results.',
283
- `Available sub-agents:\n${subAgentList}`,
284
- ].join(' ');
285
-
286
- return {
287
- scope: 'CUSTOM',
288
- execution: 'backend',
289
- defaultPermission: 'ASK',
290
- silence: false,
291
- introduction: {
292
- title: `[${leaderUsername}] Dispatch sub-agents`,
293
- about: description,
294
- },
295
- definition: {
296
- name: toolName,
297
- description,
298
- schema: z.object({
299
- tasks: z
300
- .array(
301
- z.object({
302
- subAgent: z
303
- .enum(subAgentNames as [string, ...string[]])
304
- .describe('Username of the sub-agent that should execute this task.'),
305
- task: z.string().describe('Detailed task description for the sub-agent.'),
306
- context: z.string().optional().describe('Optional additional context for the sub-agent.'),
307
- }),
308
- )
309
- .min(1)
310
- .max(MAX_DISPATCH_TASKS)
311
- .describe(`List of sub-tasks to dispatch concurrently. Up to ${MAX_DISPATCH_CONCURRENCY} run in parallel.`),
312
- }),
313
- },
314
- invoke: async (
315
- ctx: Context,
316
- args: { tasks: Array<{ subAgent: string; task: string; context?: string }> },
317
- runtime?: ToolRuntimeInput,
318
- ) => {
319
- const id = getToolCallId(runtime) || `dispatch-${Date.now()}`;
320
- const callingEmployee = await resolveCallingEmployee(ctx, plugin);
321
- if (!callingEmployee) {
322
- const distinctSubs = Array.from(new Set((args.tasks ?? []).map((t) => t.subAgent).filter(Boolean)));
323
- const reportedSub = distinctSubs.length === 1 ? distinctSubs[0] : '(multiple)';
324
- await logDelegation(ctx, plugin, {
325
- leaderUsername,
326
- subAgentUsername: reportedSub,
327
- toolName,
328
- task: truncateText(args.tasks ?? [], 2000),
329
- result: '',
330
- status: 'error',
331
- depth: (ctx as any)[ORCHESTRATOR_DEPTH_KEY] ?? 0,
332
- durationMs: 0,
333
- error: `Cannot determine calling AI employee for dispatch tool "${toolName}". Targets: ${
334
- distinctSubs.join(', ') || '(empty)'
335
- }.`,
336
- });
337
- return {
338
- status: 'error' as const,
339
- content: `Cannot determine calling AI employee for "${toolName}". Start the request from an AI Employee conversation so leader scoping can be enforced.`,
340
- };
341
- }
342
- if (callingEmployee && callingEmployee !== leaderUsername) {
343
- // Mirror the per-rule delegate tool: persist the rejection to
344
- // orchestratorLogs so admins can investigate via the Tracing tab.
345
- const distinctSubs = Array.from(new Set((args.tasks ?? []).map((t) => t.subAgent).filter(Boolean)));
346
- const reportedSub = distinctSubs.length === 1 ? distinctSubs[0] : '(multiple)';
347
- await logDelegation(ctx, plugin, {
348
- leaderUsername,
349
- subAgentUsername: reportedSub,
350
- toolName,
351
- task: truncateText(args.tasks ?? [], 2000),
352
- result: '',
353
- status: 'error',
354
- depth: (ctx as any)[ORCHESTRATOR_DEPTH_KEY] ?? 0,
355
- durationMs: 0,
356
- error: `Employee "${callingEmployee}" is not authorized to dispatch sub-agents for leader "${leaderUsername}". Targets: ${
357
- distinctSubs.join(', ') || '(empty)'
358
- }.`,
359
- });
360
- return {
361
- status: 'error' as const,
362
- content: `Employee "${callingEmployee}" is not authorized to dispatch sub-agents for leader "${leaderUsername}".`,
363
- };
364
- }
365
-
366
- const tasks = args.tasks ?? [];
367
- if (!tasks.length) {
368
- return {
369
- status: 'error' as const,
370
- content: 'No tasks provided. Pass at least one item in `tasks`.',
371
- };
372
- }
373
-
374
- const dispatchRootRunId =
375
- getOrchestratorTraceContext(ctx)?.rootRunId || createRootRunId(`${leaderUsername}:dispatch`);
376
-
377
- const results = await runWithConcurrency<
378
- { subAgent: string; task: string; context?: string },
379
- DispatchTaskResult
380
- >(tasks, MAX_DISPATCH_CONCURRENCY, async (item, i) => {
381
- const startedAt = Date.now();
382
- const entry = rulesBySubAgent.get(item.subAgent);
383
- if (!entry) {
384
- return {
385
- index: i,
386
- subAgent: item.subAgent,
387
- status: 'error',
388
- content: `Unknown sub-agent "${item.subAgent}". Allowed: ${subAgentNames.join(', ')}.`,
389
- durationMs: 0,
390
- };
391
- }
392
-
393
- try {
394
- const res = await invokeDelegateTask(ctx, plugin, {
395
- leaderUsername,
396
- subAgentUsername: item.subAgent,
397
- subAgentEmployee: entry.employee,
398
- task: item.task,
399
- context: item.context,
400
- maxDepth: entry.rule.maxDepth ?? 1,
401
- timeout: entry.rule.timeout ?? 120000,
402
- toolCallId: `${id}-${i}`,
403
- toolName,
404
- llmService: entry.rule.llmService,
405
- model: entry.rule.model,
406
- recursionLimit: entry.rule.recursionLimit,
407
- rootRunId: dispatchRootRunId,
408
- });
409
- return {
410
- index: i,
411
- subAgent: item.subAgent,
412
- status: res.status,
413
- content: res.content,
414
- durationMs: Date.now() - startedAt,
415
- };
416
- } catch (e: any) {
417
- return {
418
- index: i,
419
- subAgent: item.subAgent,
420
- status: 'error',
421
- content: e?.message || String(e),
422
- durationMs: Date.now() - startedAt,
423
- };
424
- }
425
- });
426
-
427
- const successCount = results.filter((r) => r.status === 'success').length;
428
- return {
429
- status: (successCount > 0 ? 'success' : 'error') as 'success' | 'error',
430
- content: formatDispatchResults(results, rulesBySubAgent),
431
- };
432
- },
433
- };
434
- }
435
-
436
- async function resolveCallingEmployee(ctx: Context, plugin: any) {
437
- const values = (ctx as any).action?.params?.values || {};
438
- const raw =
439
- (ctx as any)._currentAIEmployee ||
440
- (ctx as any).state?.currentAIEmployee ||
441
- (ctx as any).runtime?.context?.currentAIEmployee ||
442
- values.aiEmployee;
443
-
444
- const direct = normalizeEmployeeUsername(raw);
445
- if (direct) return direct;
446
-
447
- const sessionId = values.sessionId || (ctx as any).action?.params?.sessionId;
448
- if (!sessionId) return null;
449
-
450
- try {
451
- const repo = (ctx as any).db?.getRepository?.('aiConversations') || plugin.db.getRepository('aiConversations');
452
- const conversation = await repo.findOne({
453
- filter: { sessionId },
454
- });
455
- return normalizeEmployeeUsername(conversation?.aiEmployeeUsername || conversation?.get?.('aiEmployeeUsername'));
456
- } catch (e) {
457
- plugin.app.log.warn(`[AgentOrchestrator] Failed to resolve AI employee for session "${sessionId}"`, e);
458
- return null;
459
- }
460
- }
461
-
462
- function hasModelSettings(value: any): value is { llmService: string; model: string } {
463
- return Boolean(value?.llmService && value?.model);
13
+ export function invalidateDelegateToolsCache() {
14
+ // Legacy dynamic delegate tools are retired. Native plugin-ai owns sub-agent dispatch.
464
15
  }
465
16
 
466
17
  /**
467
- * Cache for built delegate tool descriptors to avoid re-querying DB on every
468
- * core toolsManager.listTools() call (which can fire many times per chat turn).
18
+ * Retired compatibility provider.
469
19
  *
470
- * - TTL is a safety net in case event hooks miss an external write path.
471
- * - DB hooks invalidate immediately on rule/employee changes so admin edits
472
- * take effect on the next request.
473
- */
474
- const TOOLS_CACHE_TTL_MS = 30_000;
475
- let toolsCacheByPlugin: WeakMap<object, { tools: any[]; expiresAt: number }> = new WeakMap();
476
- let hooksAttached: WeakSet<object> | null = null;
477
-
478
- function attachInvalidationHooks(plugin: any) {
479
- // Attach once per plugin instance (handles dev hot-reload safely).
480
- if (!hooksAttached) hooksAttached = new WeakSet<object>();
481
- if (hooksAttached.has(plugin)) return;
482
- hooksAttached.add(plugin);
483
-
484
- const invalidate = () => {
485
- toolsCacheByPlugin.delete(plugin);
486
- registeredDelegateNamesByPlugin.delete(plugin);
487
- };
488
- plugin.db.on('orchestratorConfig.afterCreate', invalidate);
489
- plugin.db.on('orchestratorConfig.afterUpdate', invalidate);
490
- plugin.db.on('orchestratorConfig.afterDestroy', invalidate);
491
- plugin.db.on('aiEmployees.afterCreate', invalidate);
492
- plugin.db.on('aiEmployees.afterUpdate', invalidate);
493
- plugin.db.on('aiEmployees.afterDestroy', invalidate);
494
- }
495
-
496
- async function buildDelegateTools(plugin: any) {
497
- const configRepo = plugin.db.getRepository('orchestratorConfig');
498
- if (!configRepo) {
499
- registeredDelegateNamesByPlugin.set(plugin, new Set());
500
- return [];
501
- }
502
-
503
- const configs = await configRepo.find({
504
- filter: { enabled: true },
505
- });
506
- if (!configs?.length) {
507
- registeredDelegateNamesByPlugin.set(plugin, new Set());
508
- return [];
509
- }
510
-
511
- const employeeCache = new Map<string, any>();
512
- const tools: any[] = [];
513
- // Track every generated tool name to surface sanitize() collisions
514
- // (e.g. "pm-1" and "pm.1" both → "pm_1"). Collisions are skipped + logged.
515
- const generatedNames = new Map<string, { leader: string; sub: string }>();
516
- const configsBySubAgent = new Map<string, any[]>();
517
- for (const config of configs) {
518
- const items = configsBySubAgent.get(config.subAgentUsername) || [];
519
- items.push(config);
520
- configsBySubAgent.set(config.subAgentUsername, items);
521
- }
522
-
523
- for (const config of configs) {
524
- const { leaderUsername, subAgentUsername, maxDepth, timeout, recursionLimit } = config;
525
-
526
- let subAgentEmployee = employeeCache.get(subAgentUsername);
527
- if (!subAgentEmployee) {
528
- subAgentEmployee = await plugin.db.getRepository('aiEmployees').findOne({
529
- filter: { username: subAgentUsername },
530
- });
531
- if (subAgentEmployee) {
532
- employeeCache.set(subAgentUsername, subAgentEmployee);
533
- }
534
- }
535
- if (!subAgentEmployee) continue;
536
-
537
- const toolName = buildDelegateToolName(leaderUsername, subAgentUsername);
538
- if (toolName.length > MAX_TOOL_NAME_LENGTH) {
539
- plugin.app.log.error(
540
- `[AgentOrchestrator] Tool name "${toolName}" exceeds the ${MAX_TOOL_NAME_LENGTH}-char limit enforced by most LLM providers. Skipping rule (${leaderUsername} → ${subAgentUsername}). Shorten one of the usernames.`,
541
- );
542
- continue;
543
- }
544
- const existing = generatedNames.get(toolName);
545
- if (existing) {
546
- const suffix = createHash('sha1').update(`${leaderUsername}::${subAgentUsername}`).digest('hex').slice(0, 6);
547
- plugin.app.log.error(
548
- `[AgentOrchestrator] Tool-name collision: rule (${leaderUsername} → ${subAgentUsername}) sanitizes to "${toolName}", same as (${existing.leader} → ${existing.sub}). Skipping duplicate registration. Rename one of the usernames or apply suffix "_${suffix}" manually.`,
549
- );
550
- continue;
551
- }
552
- generatedNames.set(toolName, { leader: leaderUsername, sub: subAgentUsername });
553
- tools.push(
554
- createDelegateToolOptions(plugin, {
555
- leaderUsername,
556
- subAgentUsername,
557
- subAgentEmployee,
558
- maxDepth,
559
- timeout,
560
- toolName,
561
- llmService: config.llmService,
562
- model: config.model,
563
- recursionLimit,
564
- }),
565
- );
566
- }
567
-
568
- // Compatibility for existing single-parent setups that already assigned
569
- // delegate_to_<sub> to the parent employee's skills.
570
- for (const [subAgentUsername, items] of configsBySubAgent.entries()) {
571
- if (items.length !== 1) {
572
- // Multiple leaders for the same sub-agent ⇒ alias is ambiguous.
573
- // Surface it so admins know why old skill assignments may stop working.
574
- const leaders = items.map((c: any) => c.leaderUsername).join(', ');
575
- plugin.app.log.warn(
576
- `[AgentOrchestrator] Legacy alias "delegate_to_${sanitizeToolPart(
577
- subAgentUsername,
578
- )}" is NOT registered for sub-agent "${subAgentUsername}" because it has multiple leaders (${leaders}). Leaders must use the per-rule "delegate_<leader>_to_<sub>" tool name.`,
579
- );
580
- continue;
581
- }
582
- const config = items[0];
583
- const subAgentEmployee = employeeCache.get(subAgentUsername);
584
- if (!subAgentEmployee) continue;
585
- const legacyToolName = `delegate_to_${sanitizeToolPart(subAgentUsername)}`;
586
- if (legacyToolName.length > MAX_TOOL_NAME_LENGTH) {
587
- plugin.app.log.error(
588
- `[AgentOrchestrator] Legacy alias "${legacyToolName}" exceeds the ${MAX_TOOL_NAME_LENGTH}-char limit. Skipping alias for sub-agent "${subAgentUsername}".`,
589
- );
590
- continue;
591
- }
592
- const aliasExisting = generatedNames.get(legacyToolName);
593
- if (aliasExisting) {
594
- plugin.app.log.error(
595
- `[AgentOrchestrator] Legacy alias "${legacyToolName}" collides with another rule (${aliasExisting.leader} → ${aliasExisting.sub}). Skipping alias registration.`,
596
- );
597
- continue;
598
- }
599
- generatedNames.set(legacyToolName, {
600
- leader: config.leaderUsername,
601
- sub: subAgentUsername,
602
- });
603
- tools.push(
604
- createDelegateToolOptions(plugin, {
605
- leaderUsername: config.leaderUsername,
606
- subAgentUsername,
607
- subAgentEmployee,
608
- maxDepth: config.maxDepth,
609
- timeout: config.timeout,
610
- toolName: legacyToolName,
611
- legacyAlias: true,
612
- llmService: config.llmService,
613
- model: config.model,
614
- recursionLimit: config.recursionLimit,
615
- }),
20
+ * plugin-agent-orchestrator no longer registers delegate_* or dispatch_subagents_*
21
+ * tools. Keep this export so older imports fail closed instead of reintroducing
22
+ * a LangChain-backed executor path.
23
+ */
24
+ export function createDelegateToolsProvider(plugin: { app?: { logger?: { info?: (message: string) => void } } }) {
25
+ return async () => {
26
+ plugin.app?.logger?.info?.(
27
+ '[AgentOrchestrator] Legacy delegate_* tools are retired; native dispatch-sub-agent-task is used instead.',
616
28
  );
617
- }
618
-
619
- // One dispatch fan-out tool per leader.
620
- const rulesByLeader = new Map<string, Map<string, DispatchRuleEntry>>();
621
- for (const config of configs) {
622
- const subAgentEmployee = employeeCache.get(config.subAgentUsername);
623
- if (!subAgentEmployee) continue;
624
- let bucket = rulesByLeader.get(config.leaderUsername);
625
- if (!bucket) {
626
- bucket = new Map<string, DispatchRuleEntry>();
627
- rulesByLeader.set(config.leaderUsername, bucket);
628
- }
629
- bucket.set(config.subAgentUsername, { rule: config, employee: subAgentEmployee });
630
- }
631
- for (const [leaderUsername, rulesBySubAgent] of rulesByLeader.entries()) {
632
- if (!rulesBySubAgent.size) continue;
633
- const dispatchToolName = buildDispatchToolName(leaderUsername);
634
- if (dispatchToolName.length > MAX_TOOL_NAME_LENGTH) {
635
- plugin.app.log.error(
636
- `[AgentOrchestrator] Dispatch tool "${dispatchToolName}" exceeds the ${MAX_TOOL_NAME_LENGTH}-char limit. Skipping for leader "${leaderUsername}".`,
637
- );
638
- continue;
639
- }
640
- const dispatchExisting = generatedNames.get(dispatchToolName);
641
- if (dispatchExisting) {
642
- plugin.app.log.error(
643
- `[AgentOrchestrator] Dispatch tool "${dispatchToolName}" collides with another generated tool (${dispatchExisting.leader} → ${dispatchExisting.sub}). Skipping dispatch registration for leader "${leaderUsername}".`,
644
- );
645
- continue;
646
- }
647
- generatedNames.set(dispatchToolName, { leader: leaderUsername, sub: '(dispatch)' });
648
- tools.push(createDispatchToolOptions(plugin, { leaderUsername, rulesBySubAgent }));
649
- }
650
-
651
- // Refresh the registry that `isDelegateToolName` consults so sub-agents
652
- // running concurrently filter exactly the names we just registered.
653
- registeredDelegateNamesByPlugin.set(plugin, new Set(generatedNames.keys()));
654
-
655
- return tools;
656
- }
657
-
658
- /**
659
- * Creates one dynamic tool per configured sub-agent for a given leader.
660
- * Uses Strategy B (Per-SubAgent Tool): each sub-agent becomes a separate tool
661
- * with its own name and description, making LLM tool selection natural.
662
- *
663
- * Architecture:
664
- * - Uses createReactAgent (public LangGraph API) for agent execution
665
- * - Uses plugin-ai's getLLMService() for LLM model resolution
666
- * - Uses core app.aiManager.toolsManager.listTools() for tool resolution
667
- * (same manager that AIEmployee uses — see ai-employee.ts:1286)
668
- * - Depth enforcement via ctx metadata tracking
669
- * - Per-leader scoping via invoke-time check (core ToolsOptions has no
670
- * leaderUsername field, so scoping is enforced in the invoke callback)
671
- */
672
- export function createDelegateToolsProvider(plugin: any) {
673
- attachInvalidationHooks(plugin);
674
-
675
- return async (register: any) => {
676
- try {
677
- let toolsCache = toolsCacheByPlugin.get(plugin);
678
- if (!toolsCache || toolsCache.expiresAt <= Date.now()) {
679
- const tools = await buildDelegateTools(plugin);
680
- toolsCache = { tools, expiresAt: Date.now() + TOOLS_CACHE_TTL_MS };
681
- toolsCacheByPlugin.set(plugin, toolsCache);
682
- }
683
-
684
- if (toolsCache.tools.length) {
685
- register.registerTools(toolsCache.tools);
686
- }
687
- } catch (e) {
688
- plugin.app.log.error('[AgentOrchestrator] Failed to register delegate tools', e);
689
- }
690
29
  };
691
30
  }
692
-
693
- /**
694
- * Test/internal helper to drop the in-memory tool cache (e.g., from a CLI op).
695
- */
696
- export function invalidateDelegateToolsCache() {
697
- toolsCacheByPlugin = new WeakMap();
698
- registeredDelegateNamesByPlugin = new WeakMap();
699
- }
700
-
701
- /**
702
- * Core execution logic using createReactAgent (public LangGraph API).
703
- *
704
- * This approach mirrors plugin-sub-agent's proven pattern:
705
- * 1. Get LLM model via aiPlugin.aiManager.getLLMService()
706
- * 2. Resolve sub-agent's tools via core app.aiManager.toolsManager.listTools()
707
- * 3. Build a standalone createReactAgent with the model + tools
708
- * 4. Stream results and extract final AI message
709
- *
710
- * Tool resolution uses the CORE toolsManager (app.aiManager.toolsManager) —
711
- * the same manager that AIEmployee.getToolsMap() uses (see ai-employee.ts:1286).
712
- * This ensures tool names in skillSettings.tools[].name match correctly.
713
- *
714
- * skillSettings.tools shape:
715
- * { name: string, autoCall: boolean }[]
716
- */
717
- async function invokeDelegateTask(
718
- ctx: Context,
719
- plugin: any,
720
- options: {
721
- leaderUsername: string;
722
- subAgentUsername: string;
723
- subAgentEmployee: any;
724
- task: string;
725
- context?: string;
726
- maxDepth: number;
727
- timeout: number;
728
- toolCallId: string;
729
- toolName: string;
730
- llmService?: string;
731
- model?: string;
732
- recursionLimit?: number;
733
- rootRunId?: string;
734
- parentSpanId?: string;
735
- },
736
- ) {
737
- const {
738
- leaderUsername,
739
- subAgentUsername,
740
- subAgentEmployee,
741
- task,
742
- context,
743
- maxDepth,
744
- timeout,
745
- toolCallId,
746
- toolName,
747
- llmService,
748
- model,
749
- recursionLimit,
750
- rootRunId: providedRootRunId,
751
- parentSpanId: providedParentSpanId,
752
- } = options;
753
-
754
- // --- Snapshot ctx fields up-front ---
755
- const ctxSnapshot = captureCtxSnapshot(ctx);
756
-
757
- // --- Depth enforcement & Circular Delegation Detection ---
758
- const currentDepth: number = (ctx as any)[ORCHESTRATOR_DEPTH_KEY] ?? 0;
759
- const currentPath: string[] = (ctx as any)[ORCHESTRATOR_PATH_KEY] ?? [leaderUsername];
760
-
761
- if (currentPath.includes(subAgentUsername)) {
762
- const loopChain = [...currentPath, subAgentUsername].join(' -> ');
763
- await logDelegation(ctx, plugin, {
764
- leaderUsername,
765
- subAgentUsername,
766
- toolName,
767
- task,
768
- context,
769
- result: '',
770
- status: 'error',
771
- depth: currentDepth,
772
- durationMs: 0,
773
- error: `Circular delegation detected: ${loopChain}.`,
774
- snapshot: ctxSnapshot,
775
- });
776
- return {
777
- status: 'error' as const,
778
- content: `Circular delegation detected: ${loopChain}. Execution aborted to prevent infinite reasoning loops.`,
779
- };
780
- }
781
-
782
- if (currentDepth >= maxDepth) {
783
- await logDelegation(ctx, plugin, {
784
- leaderUsername,
785
- subAgentUsername,
786
- toolName,
787
- task,
788
- context,
789
- result: '',
790
- status: 'error',
791
- depth: currentDepth,
792
- durationMs: 0,
793
- error: `Delegation depth limit reached (${currentDepth}/${maxDepth}).`,
794
- snapshot: ctxSnapshot,
795
- });
796
- return {
797
- status: 'error' as const,
798
- content: `Delegation depth limit reached (${currentDepth}/${maxDepth}). Sub-agent "${subAgentUsername}" cannot delegate further.`,
799
- };
800
- }
801
-
802
- const upstreamTraceContext = getOrchestratorTraceContext(ctx);
803
- const rootRunId =
804
- providedRootRunId || upstreamTraceContext?.rootRunId || createRootRunId(`${leaderUsername}:${subAgentUsername}`);
805
- const parentSpanId = providedParentSpanId || upstreamTraceContext?.spanId || upstreamTraceContext?.parentSpanId;
806
- const agentLoopRunId = upstreamTraceContext?.agentLoopRunId;
807
- const agentLoopStepId = upstreamTraceContext?.agentLoopStepId;
808
-
809
- return plugin.agentLoopService.harness.runSubAgent(ctx, {
810
- leaderUsername,
811
- subAgentUsername,
812
- subAgentEmployee,
813
- task,
814
- context,
815
- currentDepth,
816
- currentPath,
817
- maxDepth,
818
- timeout,
819
- toolCallId,
820
- toolName,
821
- llmService,
822
- model,
823
- recursionLimit,
824
- rootRunId,
825
- parentSpanId,
826
- agentLoopRunId,
827
- agentLoopStepId,
828
- });
829
- }
830
-
831
- /**
832
- * Log a delegation event to the orchestratorLogs collection for observability.
833
- */
834
- async function logDelegation(
835
- ctx: Context,
836
- plugin: any,
837
- data: {
838
- id?: number | string;
839
- leaderUsername: string;
840
- subAgentUsername: string;
841
- toolName: string;
842
- task: string;
843
- context?: string;
844
- result: string;
845
- status: string;
846
- depth: number;
847
- durationMs: number;
848
- error?: string;
849
- trace?: TraceEvent[];
850
- messages?: any[];
851
- snapshot?: CtxSnapshot;
852
- },
853
- ) {
854
- // Capture userId from snapshot or ctx
855
- let userId: number | string | undefined = data.snapshot?.userId;
856
- if (userId == null) {
857
- try {
858
- userId = (ctx as any).auth?.user?.id || (ctx as any).state?.currentUser?.id;
859
- } catch {
860
- // ctx lifecycle ended
861
- }
862
- }
863
- return sharedLogDelegation(ctx, plugin, { ...data, userId });
864
- }