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.
- package/README.md +9 -7
- package/dist/client/index.js +1 -1
- package/dist/client-v2/{214.723affb37c13bf7a.js → 214.79650a549273f163.js} +1 -1
- package/dist/client-v2/264.718a107e43fc163c.js +10 -0
- package/dist/client-v2/373.f5d5292e53c4e832.js +10 -0
- package/dist/client-v2/{41.1805b2edfaa4afe2.js → 41.ba6e080cc0488143.js} +1 -1
- package/dist/client-v2/418.29e713f79131eece.js +10 -0
- package/dist/client-v2/619.bd3c5698b40705c3.js +10 -0
- package/dist/client-v2/677.a991ce0250ff5c77.js +10 -0
- package/dist/client-v2/{70.a15d7fcec7c41768.js → 70.bda9518881c05360.js} +1 -1
- package/dist/client-v2/925.f5370de8f6632d65.js +10 -0
- package/dist/client-v2/index.js +1 -1
- package/dist/externalVersion.js +7 -10
- package/dist/locale/en-US.json +94 -25
- package/dist/locale/vi-VN.json +94 -25
- package/dist/locale/zh-CN.json +94 -25
- package/dist/server/collections/agent-execution-spans.js +37 -0
- package/dist/server/collections/agent-harness-profiles.js +2 -2
- package/dist/server/collections/agent-memory-contexts.js +125 -0
- package/dist/server/collections/orchestrator-logs.js +2 -2
- package/dist/server/migrations/20260425000000-add-interaction-schema.js +3 -1
- package/dist/server/migrations/20260427000000-change-packages-to-text.js +3 -1
- package/dist/server/migrations/20260427000001-change-other-json-to-text.js +6 -2
- package/dist/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.js +21 -19
- package/dist/server/migrations/20260621000000-native-policy-profile-defaults.js +193 -0
- package/dist/server/plugin.js +128 -74
- package/dist/server/resources/agent-monitor.js +454 -0
- package/dist/server/services/AgentHarness.js +24 -499
- package/dist/server/services/AgentMemoryContextService.js +216 -0
- package/dist/server/services/ExecutionSpanService.js +2 -2
- package/dist/server/services/NativeSubAgentObserver.js +413 -0
- package/dist/server/skill-hub/plugin.js +81 -5
- package/dist/server/skill-hub/tasks/SkillExecutionTask.js +9 -3
- package/dist/server/tools/delegate-task.js +11 -589
- package/dist/server/utils/skill-settings.js +18 -1
- package/package.json +47 -49
- package/src/client/AIEmployeesContext.tsx +5 -18
- package/src/client/AgentRunsTab.tsx +2 -771
- package/src/client/HarnessProfilesTab.tsx +2 -257
- package/src/client/OrchestratorSettings.tsx +97 -106
- package/src/client/RulesTab.tsx +2 -788
- package/src/client/plugin.tsx +0 -2
- package/src/client/skill-hub/components/ExecutionHistory.tsx +200 -202
- package/src/client/skill-hub/components/ExecutionProgress.tsx +51 -55
- package/src/client/skill-hub/components/LoopSettings.tsx +331 -331
- package/src/client/skill-hub/components/SkillEditor.tsx +43 -39
- package/src/client/skill-hub/components/SkillManager.tsx +194 -181
- package/src/client/skill-hub/components/SkillTestPanel.tsx +141 -145
- package/src/client/skill-hub/locale.ts +16 -16
- package/src/client/skill-hub/tools/SkillHubCard.tsx +104 -109
- package/src/client/skill-hub/tools/loopTemplates.ts +52 -52
- package/src/client/skill-hub/utils/jsonFields.ts +7 -3
- package/src/client-v2/components/AIEmployeesContext.tsx +3 -16
- package/src/client-v2/components/AgentRunsTab.tsx +182 -455
- package/src/client-v2/components/HarnessProfilesTab.tsx +34 -31
- package/src/client-v2/components/RulesTab.tsx +2 -782
- package/src/client-v2/components/TracingTab.tsx +1 -1
- package/src/client-v2/hooks/useApiRequest.ts +8 -1
- package/src/client-v2/pages/RulesPage.tsx +2 -2
- package/src/client-v2/plugin.tsx +3 -3
- package/src/locale/en-US.json +94 -25
- package/src/locale/vi-VN.json +94 -25
- package/src/locale/zh-CN.json +94 -25
- package/src/server/__tests__/native-sub-agent-observer.test.ts +246 -0
- package/src/server/__tests__/skill-settings.test.ts +6 -6
- package/src/server/__tests__/smoke.test.ts +1 -0
- package/src/server/collections/agent-execution-spans.ts +37 -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-steps.ts +144 -144
- package/src/server/collections/agent-memory-contexts.ts +95 -0
- package/src/server/collections/orchestrator-logs.ts +4 -4
- package/src/server/collections/skill-definitions.ts +111 -111
- package/src/server/collections/skill-executions.ts +106 -106
- package/src/server/collections/skill-loop-configs.ts +65 -65
- package/src/server/migrations/20260423000000-add-progress-fields.ts +14 -14
- package/src/server/migrations/20260425000000-add-interaction-schema.ts +3 -1
- package/src/server/migrations/20260427000000-change-packages-to-text.ts +4 -2
- package/src/server/migrations/20260427000001-change-other-json-to-text.ts +9 -5
- 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 +145 -142
- package/src/server/migrations/20260615000000-normalize-ai-employee-tool-bindings.ts +2 -2
- package/src/server/migrations/20260621000000-native-policy-profile-defaults.ts +193 -0
- package/src/server/plugin.ts +151 -94
- package/src/server/resources/agent-monitor.ts +482 -0
- package/src/server/services/AgentHarness.ts +38 -623
- package/src/server/services/AgentMemoryContextService.ts +256 -0
- package/src/server/services/AgentPlanValidator.ts +73 -73
- package/src/server/services/ExecutionSpanService.ts +6 -2
- package/src/server/services/FileManager.ts +144 -144
- package/src/server/services/NativeSubAgentObserver.ts +507 -0
- package/src/server/services/SkillManager.ts +583 -583
- package/src/server/services/SkillRepositoryService.ts +5 -7
- package/src/server/services/TokenTracker.ts +3 -3
- package/src/server/services/WorkerEnvManager.ts +1 -2
- package/src/server/skill-hub/actions/git-import.ts +5 -7
- package/src/server/skill-hub/plugin.ts +89 -6
- package/src/server/skill-hub/tasks/SkillExecutionTask.ts +470 -460
- package/src/server/skill-hub/utils/json-fields.ts +1 -1
- package/src/server/tools/delegate-task.ts +13 -847
- package/src/server/utils/skill-settings.ts +24 -6
- package/dist/client-v2/264.0533912e6c5ea2d7.js +0 -10
- package/dist/client-v2/418.5ae055abf141820e.js +0 -10
- package/dist/client-v2/619.d99d3c9e61c99064.js +0 -10
- package/dist/client-v2/892.72db4161511c8a16.js +0 -10
- package/dist/client-v2/926.87f660b670d85bcc.js +0 -10
- package/src/client/tools/PlanApprovalCard.tsx +0 -176
- 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
|
|
61
|
-
|
|
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
|
-
*
|
|
468
|
-
* core toolsManager.listTools() call (which can fire many times per chat turn).
|
|
18
|
+
* Retired compatibility provider.
|
|
469
19
|
*
|
|
470
|
-
* -
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*/
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
}
|