plugin-agent-orchestrator 1.0.13 → 1.0.15
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 +16 -291
- package/dist/client/AIEmployeesContext.d.ts +7 -0
- package/dist/client/OrchestratorSettings.d.ts +2 -1
- package/dist/client/index.js +1 -1
- package/dist/client/plugin.d.ts +1 -0
- package/dist/client/skill-hub/components/ExecutionHistory.d.ts +2 -0
- package/dist/client/skill-hub/components/ExecutionProgress.d.ts +20 -0
- package/dist/client/skill-hub/components/GitSkillImport.d.ts +7 -0
- package/dist/client/skill-hub/components/SkillEditor.d.ts +7 -0
- package/dist/client/skill-hub/components/SkillManager.d.ts +2 -0
- package/dist/client/skill-hub/components/SkillMetrics.d.ts +2 -0
- package/dist/client/skill-hub/components/SkillTestPanel.d.ts +7 -0
- package/dist/client/skill-hub/index.d.ts +10 -0
- package/dist/client/skill-hub/locale.d.ts +3 -0
- package/dist/client/skill-hub/tools/InteractionSchemasProvider.d.ts +19 -0
- package/dist/client/skill-hub/tools/SkillHubCard.d.ts +3 -0
- package/dist/client/skill-hub/utils/jsonFields.d.ts +3 -0
- package/dist/externalVersion.js +6 -6
- package/dist/node_modules/adm-zip/LICENSE +21 -0
- package/dist/node_modules/adm-zip/adm-zip.js +1 -0
- package/dist/node_modules/adm-zip/headers/entryHeader.js +377 -0
- package/dist/node_modules/adm-zip/headers/index.js +2 -0
- package/dist/node_modules/adm-zip/headers/mainHeader.js +130 -0
- package/dist/node_modules/adm-zip/methods/deflater.js +33 -0
- package/dist/node_modules/adm-zip/methods/index.js +3 -0
- package/dist/node_modules/adm-zip/methods/inflater.js +34 -0
- package/dist/node_modules/adm-zip/methods/zipcrypto.js +175 -0
- package/dist/node_modules/adm-zip/package.json +1 -0
- package/dist/node_modules/adm-zip/util/constants.js +142 -0
- package/dist/node_modules/adm-zip/util/decoder.js +5 -0
- package/dist/node_modules/adm-zip/util/errors.js +63 -0
- package/dist/node_modules/adm-zip/util/fattr.js +76 -0
- package/dist/node_modules/adm-zip/util/index.js +5 -0
- package/dist/node_modules/adm-zip/util/utils.js +339 -0
- package/dist/node_modules/adm-zip/zipEntry.js +405 -0
- package/dist/node_modules/adm-zip/zipFile.js +446 -0
- package/dist/node_modules/simple-git/dist/cjs/index.js +7399 -0
- package/dist/node_modules/simple-git/dist/esm/index.js +4745 -0
- package/dist/node_modules/simple-git/dist/esm/package.json +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/api.d.ts +13 -0
- package/dist/node_modules/simple-git/dist/src/lib/args/log-format.d.ts +9 -0
- package/dist/node_modules/simple-git/dist/src/lib/errors/git-construct-error.d.ts +15 -0
- package/dist/node_modules/simple-git/dist/src/lib/errors/git-error.d.ts +30 -0
- package/dist/node_modules/simple-git/dist/src/lib/errors/git-plugin-error.d.ts +7 -0
- package/dist/node_modules/simple-git/dist/src/lib/errors/git-response-error.d.ts +32 -0
- package/dist/node_modules/simple-git/dist/src/lib/errors/task-configuration-error.d.ts +12 -0
- package/dist/node_modules/simple-git/dist/src/lib/git-factory.d.ts +15 -0
- package/dist/node_modules/simple-git/dist/src/lib/git-logger.d.ts +21 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-branch-delete.d.ts +5 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-branch.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-commit.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-diff-summary.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-fetch.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-list-log-summary.d.ts +6 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-merge.d.ts +11 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-move.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-pull.d.ts +6 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-push.d.ts +4 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-remote-messages.d.ts +5 -0
- package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-remote-objects.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/abort-plugin.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/block-unsafe-operations-plugin.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/command-config-prefixing-plugin.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/completion-detection.plugin.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/custom-binary.plugin.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/error-detection.plugin.d.ts +7 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/index.d.ts +11 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/plugin-store.d.ts +11 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/progress-monitor-plugin.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/simple-git-plugin.d.ts +48 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/spawn-options-plugin.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/suffix-paths.plugin.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/plugins/timout-plugin.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/BranchDeleteSummary.d.ts +12 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/BranchSummary.d.ts +14 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/CheckIgnore.d.ts +4 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/CleanSummary.d.ts +9 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/ConfigList.d.ts +13 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/DiffSummary.d.ts +10 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/FileStatusSummary.d.ts +9 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/GetRemoteSummary.d.ts +11 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/InitSummary.d.ts +9 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/MergeSummary.d.ts +16 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/PullSummary.d.ts +25 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/StatusSummary.d.ts +19 -0
- package/dist/node_modules/simple-git/dist/src/lib/responses/TagList.d.ts +7 -0
- package/dist/node_modules/simple-git/dist/src/lib/runners/git-executor-chain.d.ts +25 -0
- package/dist/node_modules/simple-git/dist/src/lib/runners/git-executor.d.ts +14 -0
- package/dist/node_modules/simple-git/dist/src/lib/runners/promise-wrapped.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/runners/scheduler.d.ts +11 -0
- package/dist/node_modules/simple-git/dist/src/lib/runners/tasks-pending-queue.d.ts +23 -0
- package/dist/node_modules/simple-git/dist/src/lib/simple-git-api.d.ts +20 -0
- package/dist/node_modules/simple-git/dist/src/lib/task-callback.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/apply-patch.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/branch.d.ts +7 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/change-working-directory.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/check-ignore.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/check-is-repo.d.ts +9 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/checkout.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/clean.d.ts +25 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/clone.d.ts +9 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/commit.d.ts +4 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/config.d.ts +8 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/count-objects.d.ts +12 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/diff-name-status.d.ts +12 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/diff.d.ts +5 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/fetch.d.ts +4 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/first-commit.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/grep.d.ts +12 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/hash-object.d.ts +5 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/init.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/log.d.ts +32 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/merge.d.ts +4 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/move.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/pull.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/push.d.ts +9 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/remote.d.ts +8 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/reset.d.ts +11 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/show.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/stash-list.d.ts +4 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/status.d.ts +3 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/sub-module.d.ts +5 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/tag.d.ts +18 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/task.d.ts +14 -0
- package/dist/node_modules/simple-git/dist/src/lib/tasks/version.d.ts +9 -0
- package/dist/node_modules/simple-git/dist/src/lib/types/handlers.d.ts +21 -0
- package/dist/node_modules/simple-git/dist/src/lib/types/index.d.ts +136 -0
- package/dist/node_modules/simple-git/dist/src/lib/types/tasks.d.ts +19 -0
- package/dist/node_modules/simple-git/dist/src/lib/utils/argument-filters.d.ts +14 -0
- package/dist/node_modules/simple-git/dist/src/lib/utils/exit-codes.d.ts +10 -0
- package/dist/node_modules/simple-git/dist/src/lib/utils/git-output-streams.d.ts +7 -0
- package/dist/node_modules/simple-git/dist/src/lib/utils/index.d.ts +8 -0
- package/dist/node_modules/simple-git/dist/src/lib/utils/line-parser.d.ts +15 -0
- package/dist/node_modules/simple-git/dist/src/lib/utils/simple-git-options.d.ts +2 -0
- package/dist/node_modules/simple-git/dist/src/lib/utils/task-options.d.ts +13 -0
- package/dist/node_modules/simple-git/dist/src/lib/utils/task-parser.d.ts +5 -0
- package/dist/node_modules/simple-git/dist/src/lib/utils/util.d.ts +47 -0
- package/dist/node_modules/simple-git/dist/typings/errors.d.ts +5 -0
- package/dist/node_modules/simple-git/dist/typings/index.d.ts +14 -0
- package/dist/node_modules/simple-git/dist/typings/response.d.ts +556 -0
- package/dist/node_modules/simple-git/dist/typings/simple-git.d.ts +1033 -0
- package/dist/node_modules/simple-git/dist/typings/types.d.ts +22 -0
- package/dist/node_modules/simple-git/node_modules/debug/package.json +64 -0
- package/dist/node_modules/simple-git/node_modules/debug/src/browser.js +272 -0
- package/dist/node_modules/simple-git/node_modules/debug/src/common.js +292 -0
- package/dist/node_modules/simple-git/node_modules/debug/src/index.js +10 -0
- package/dist/node_modules/simple-git/node_modules/debug/src/node.js +263 -0
- package/dist/node_modules/simple-git/package.json +1 -0
- package/dist/node_modules/simple-git/promise.js +17 -0
- package/dist/server/collections/agent-execution-spans.d.ts +9 -0
- package/dist/server/collections/agent-execution-spans.js +152 -0
- package/dist/server/collections/orchestrator-config.d.ts +1 -1
- package/dist/server/collections/orchestrator-config.js +6 -0
- package/dist/server/collections/orchestrator-logs.d.ts +1 -1
- package/dist/server/collections/skill-definitions.d.ts +2 -0
- package/dist/server/collections/skill-definitions.js +158 -0
- package/dist/server/collections/skill-executions.d.ts +2 -0
- package/dist/server/collections/skill-executions.js +123 -0
- package/dist/server/collections/skill-worker-configs.d.ts +2 -0
- package/dist/server/collections/skill-worker-configs.js +115 -0
- package/dist/server/migrations/20260423000000-add-progress-fields.d.ts +4 -0
- package/dist/server/migrations/20260423000000-add-progress-fields.js +69 -0
- package/dist/server/migrations/20260425000000-add-interaction-schema.d.ts +4 -0
- package/dist/server/migrations/20260425000000-add-interaction-schema.js +61 -0
- package/dist/server/migrations/20260427000000-change-packages-to-text.d.ts +4 -0
- package/dist/server/migrations/20260427000000-change-packages-to-text.js +70 -0
- package/dist/server/migrations/20260427000001-change-other-json-to-text.d.ts +4 -0
- package/dist/server/migrations/20260427000001-change-other-json-to-text.js +80 -0
- package/dist/server/migrations/20260429000000-add-llm-fields.js +8 -0
- package/dist/server/migrations/20260429000000-fix-inputargs-json-to-text.d.ts +16 -0
- package/dist/server/migrations/20260429000000-fix-inputargs-json-to-text.js +51 -0
- package/dist/server/migrations/20260503000000-add-orchestrator-trace-fields.d.ts +7 -0
- package/dist/server/migrations/20260503000000-add-orchestrator-trace-fields.js +57 -0
- package/dist/server/plugin.d.ts +3 -0
- package/dist/server/plugin.js +37 -1
- package/dist/server/resources/tracing.js +154 -11
- package/dist/server/services/CodeValidator.d.ts +32 -0
- package/dist/server/services/CodeValidator.js +206 -0
- package/dist/server/services/ExecutionSpanService.d.ts +44 -0
- package/dist/server/services/ExecutionSpanService.js +104 -0
- package/dist/server/services/FileManager.d.ts +28 -0
- package/dist/server/services/FileManager.js +151 -0
- package/dist/server/services/SandboxRunner.d.ts +41 -0
- package/dist/server/services/SandboxRunner.js +167 -0
- package/dist/server/services/SkillManager.d.ts +6 -0
- package/dist/server/services/SkillManager.js +640 -0
- package/dist/server/services/SkillRepositoryService.d.ts +22 -0
- package/dist/server/services/SkillRepositoryService.js +157 -0
- package/dist/server/services/WorkerEnvManager.d.ts +26 -0
- package/dist/server/services/WorkerEnvManager.js +120 -0
- package/dist/server/skill-hub/actions/git-import.d.ts +21 -0
- package/dist/server/skill-hub/actions/git-import.js +413 -0
- package/dist/server/skill-hub/mcp/McpController.d.ts +15 -0
- package/dist/server/skill-hub/mcp/McpController.js +111 -0
- package/dist/server/skill-hub/plugin.d.ts +58 -0
- package/dist/server/skill-hub/plugin.js +694 -0
- package/dist/server/skill-hub/sandbox-config.json +6 -0
- package/dist/server/skill-hub/tasks/SkillExecutionTask.d.ts +16 -0
- package/dist/server/skill-hub/tasks/SkillExecutionTask.js +389 -0
- package/dist/server/skill-hub/utils/json-fields.d.ts +7 -0
- package/dist/server/skill-hub/utils/json-fields.js +88 -0
- package/dist/server/tools/delegate-task.d.ts +4 -0
- package/dist/server/tools/delegate-task.js +606 -104
- package/dist/server/tools/skill-execute.d.ts +36 -0
- package/dist/server/tools/skill-execute.js +167 -0
- package/package.json +3 -1
- package/src/client/AIEmployeeSelect.tsx +1 -3
- package/src/client/AIEmployeesContext.tsx +28 -13
- package/src/client/OrchestratorSettings.tsx +43 -5
- package/src/client/RulesTab.tsx +253 -32
- package/src/client/TracingTab.tsx +277 -213
- package/src/client/index.tsx +1 -1
- package/src/client/plugin.tsx +54 -15
- package/src/client/skill-hub/components/ExecutionHistory.tsx +201 -0
- package/src/client/skill-hub/components/ExecutionProgress.tsx +55 -0
- package/src/client/skill-hub/components/GitSkillImport.tsx +555 -0
- package/src/client/skill-hub/components/SkillEditor.tsx +456 -0
- package/src/client/skill-hub/components/SkillManager.tsx +181 -0
- package/src/client/skill-hub/components/SkillMetrics.tsx +124 -0
- package/src/client/skill-hub/components/SkillTestPanel.tsx +144 -0
- package/src/client/skill-hub/index.tsx +75 -0
- package/src/client/skill-hub/locale.ts +16 -0
- package/src/client/skill-hub/tools/InteractionSchemasProvider.tsx +59 -0
- package/src/client/skill-hub/tools/SkillHubCard.tsx +78 -0
- package/src/client/skill-hub/utils/jsonFields.ts +37 -0
- package/src/server/collections/agent-execution-spans.ts +129 -0
- package/src/server/collections/orchestrator-config.ts +7 -0
- package/src/server/collections/skill-definitions.ts +128 -0
- package/src/server/collections/skill-executions.ts +94 -0
- package/src/server/collections/skill-worker-configs.ts +86 -0
- package/src/server/migrations/20260423000000-add-progress-fields.ts +50 -0
- package/src/server/migrations/20260425000000-add-interaction-schema.ts +35 -0
- package/src/server/migrations/20260427000000-add-tracing-detail-fields.ts +5 -5
- package/src/server/migrations/20260427000000-change-packages-to-text.ts +47 -0
- package/src/server/migrations/20260427000001-change-other-json-to-text.ts +57 -0
- package/src/server/migrations/20260429000000-add-llm-fields.ts +11 -2
- package/src/server/migrations/20260429000000-fix-inputargs-json-to-text.ts +38 -0
- package/src/server/migrations/20260503000000-add-orchestrator-trace-fields.ts +32 -0
- package/src/server/plugin.ts +94 -46
- package/src/server/resources/tracing.ts +182 -15
- package/src/server/services/CodeValidator.ts +159 -0
- package/src/server/services/ExecutionSpanService.ts +106 -0
- package/src/server/services/FileManager.ts +144 -0
- package/src/server/services/SandboxRunner.ts +205 -0
- package/src/server/services/SkillManager.ts +623 -0
- package/src/server/services/SkillRepositoryService.ts +142 -0
- package/src/server/services/WorkerEnvManager.ts +113 -0
- package/src/server/skill-hub/actions/git-import.ts +486 -0
- package/src/server/skill-hub/mcp/McpController.ts +86 -0
- package/src/server/skill-hub/plugin.ts +771 -0
- package/src/server/skill-hub/sandbox-config.json +6 -0
- package/src/server/skill-hub/tasks/SkillExecutionTask.ts +443 -0
- package/src/server/skill-hub/utils/json-fields.ts +57 -0
- package/src/server/tools/delegate-task.ts +803 -127
- package/src/server/tools/skill-execute.ts +157 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
2
3
|
import { Context } from '@nocobase/actions';
|
|
3
4
|
// @ts-ignore - subpath export types resolve at build time via NocoBase bundler
|
|
4
5
|
import { createReactAgent } from '@langchain/langgraph/prebuilt';
|
|
@@ -6,6 +7,11 @@ import { DynamicStructuredTool } from '@langchain/core/tools';
|
|
|
6
7
|
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
|
|
7
8
|
import type PluginAIServer from '@nocobase/plugin-ai/dist/server';
|
|
8
9
|
import type { ToolsEntry } from '@nocobase/ai';
|
|
10
|
+
import {
|
|
11
|
+
ExecutionSpanService,
|
|
12
|
+
getOrchestratorTraceContext,
|
|
13
|
+
setOrchestratorTraceContext,
|
|
14
|
+
} from '../services/ExecutionSpanService';
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
17
|
* Maximum delegation depth key stored in ctx metadata.
|
|
@@ -13,6 +19,13 @@ import type { ToolsEntry } from '@nocobase/ai';
|
|
|
13
19
|
*/
|
|
14
20
|
const ORCHESTRATOR_DEPTH_KEY = '__orchestratorDepth';
|
|
15
21
|
|
|
22
|
+
/** Max sub-agents that the dispatch tool runs concurrently in one call. */
|
|
23
|
+
const MAX_DISPATCH_CONCURRENCY = 5;
|
|
24
|
+
/** Hard cap on tasks per dispatch call to keep output bounded. */
|
|
25
|
+
const MAX_DISPATCH_TASKS = 20;
|
|
26
|
+
/** OpenAI/Anthropic tool-name limit. Names exceeding this are silently rejected by providers. */
|
|
27
|
+
const MAX_TOOL_NAME_LENGTH = 64;
|
|
28
|
+
|
|
16
29
|
type TraceEvent = {
|
|
17
30
|
type: string;
|
|
18
31
|
at: string;
|
|
@@ -28,6 +41,11 @@ type AgentExecutionResult = {
|
|
|
28
41
|
messages: any[];
|
|
29
42
|
};
|
|
30
43
|
|
|
44
|
+
type EmployeeSkillConfig = {
|
|
45
|
+
name: string;
|
|
46
|
+
autoCall: boolean;
|
|
47
|
+
};
|
|
48
|
+
|
|
31
49
|
function sanitizeToolPart(value: string) {
|
|
32
50
|
return (value || '').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
33
51
|
}
|
|
@@ -36,8 +54,51 @@ function buildDelegateToolName(leaderUsername: string, subAgentUsername: string)
|
|
|
36
54
|
return `delegate_${sanitizeToolPart(leaderUsername)}_to_${sanitizeToolPart(subAgentUsername)}`;
|
|
37
55
|
}
|
|
38
56
|
|
|
39
|
-
function
|
|
40
|
-
return
|
|
57
|
+
function buildDispatchToolName(leaderUsername: string) {
|
|
58
|
+
return `dispatch_subagents_${sanitizeToolPart(leaderUsername)}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createRootRunId(seed = '') {
|
|
62
|
+
const hash = createHash('sha1')
|
|
63
|
+
.update(`${Date.now()}::${Math.random()}::${seed}`)
|
|
64
|
+
.digest('hex')
|
|
65
|
+
.slice(0, 10);
|
|
66
|
+
return `run_${Date.now()}_${hash}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Set of tool names this plugin actually registered in the most recent build.
|
|
71
|
+
* Sub-agents must not see these tools (would enable circular delegation), but
|
|
72
|
+
* we don't want to drop unrelated user tools whose names happen to start with
|
|
73
|
+
* "delegate_" — so we filter by the known registry, not a regex pattern.
|
|
74
|
+
*/
|
|
75
|
+
let registeredDelegateNamesByPlugin: WeakMap<object, ReadonlySet<string>> = new WeakMap();
|
|
76
|
+
|
|
77
|
+
function isDelegateToolName(plugin: any, toolName: string) {
|
|
78
|
+
return registeredDelegateNamesByPlugin.get(plugin)?.has(toolName) ?? false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Run async work over `items` with at most `limit` concurrent executions.
|
|
83
|
+
* Preserves input order in the returned array.
|
|
84
|
+
*/
|
|
85
|
+
async function runWithConcurrency<T, R>(
|
|
86
|
+
items: T[],
|
|
87
|
+
limit: number,
|
|
88
|
+
fn: (item: T, index: number) => Promise<R>,
|
|
89
|
+
): Promise<R[]> {
|
|
90
|
+
const results: R[] = new Array(items.length);
|
|
91
|
+
let cursor = 0;
|
|
92
|
+
const workerCount = Math.max(1, Math.min(limit, items.length));
|
|
93
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
94
|
+
while (cursor < items.length) {
|
|
95
|
+
const i = cursor;
|
|
96
|
+
cursor += 1;
|
|
97
|
+
results[i] = await fn(items[i], i);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
await Promise.all(workers);
|
|
101
|
+
return results;
|
|
41
102
|
}
|
|
42
103
|
|
|
43
104
|
function createDelegateToolOptions(
|
|
@@ -52,15 +113,31 @@ function createDelegateToolOptions(
|
|
|
52
113
|
legacyAlias?: boolean;
|
|
53
114
|
llmService?: string;
|
|
54
115
|
model?: string;
|
|
116
|
+
recursionLimit?: number;
|
|
55
117
|
},
|
|
56
118
|
) {
|
|
57
|
-
const {
|
|
119
|
+
const {
|
|
120
|
+
leaderUsername,
|
|
121
|
+
subAgentUsername,
|
|
122
|
+
subAgentEmployee,
|
|
123
|
+
maxDepth,
|
|
124
|
+
timeout,
|
|
125
|
+
toolName,
|
|
126
|
+
legacyAlias,
|
|
127
|
+
llmService,
|
|
128
|
+
model,
|
|
129
|
+
recursionLimit,
|
|
130
|
+
} = options;
|
|
131
|
+
const dispatchToolName = buildDispatchToolName(leaderUsername);
|
|
58
132
|
const toolDescription = [
|
|
59
133
|
`Delegate a task from "${leaderUsername}" to the AI Employee "${subAgentEmployee.nickname || subAgentUsername}".`,
|
|
60
134
|
legacyAlias ? 'This is a backward-compatible alias for existing skill assignments.' : '',
|
|
61
135
|
subAgentEmployee.about ? `Specialist profile: ${subAgentEmployee.about.substring(0, 200)}` : '',
|
|
62
136
|
'The sub-agent will execute the task independently and return its final answer.',
|
|
63
|
-
|
|
137
|
+
`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.`,
|
|
138
|
+
]
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.join(' ');
|
|
64
141
|
|
|
65
142
|
return {
|
|
66
143
|
scope: 'CUSTOM',
|
|
@@ -76,11 +153,32 @@ function createDelegateToolOptions(
|
|
|
76
153
|
description: toolDescription,
|
|
77
154
|
schema: z.object({
|
|
78
155
|
task: z.string().describe('The detailed task description for the sub-agent to execute.'),
|
|
79
|
-
context: z
|
|
156
|
+
context: z
|
|
157
|
+
.string()
|
|
158
|
+
.optional()
|
|
159
|
+
.describe('Optional additional context to help the sub-agent understand the task better.'),
|
|
80
160
|
}),
|
|
81
161
|
},
|
|
82
162
|
invoke: async (ctx: Context, args: { task: string; context?: string }, id: string) => {
|
|
83
|
-
const callingEmployee = resolveCallingEmployee(ctx);
|
|
163
|
+
const callingEmployee = await resolveCallingEmployee(ctx, plugin);
|
|
164
|
+
if (!callingEmployee) {
|
|
165
|
+
await logDelegation(ctx, plugin, {
|
|
166
|
+
leaderUsername,
|
|
167
|
+
subAgentUsername,
|
|
168
|
+
toolName,
|
|
169
|
+
task: args.task,
|
|
170
|
+
context: args.context,
|
|
171
|
+
result: '',
|
|
172
|
+
status: 'error',
|
|
173
|
+
depth: (ctx as any)[ORCHESTRATOR_DEPTH_KEY] ?? 0,
|
|
174
|
+
durationMs: 0,
|
|
175
|
+
error: `Cannot determine calling AI employee for delegation tool "${toolName}".`,
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
status: 'error' as const,
|
|
179
|
+
content: `Cannot determine calling AI employee for "${toolName}". Start the request from an AI Employee conversation so leader scoping can be enforced.`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
84
182
|
if (callingEmployee && callingEmployee !== leaderUsername) {
|
|
85
183
|
await logDelegation(ctx, plugin, {
|
|
86
184
|
leaderUsername,
|
|
@@ -112,18 +210,280 @@ function createDelegateToolOptions(
|
|
|
112
210
|
toolName,
|
|
113
211
|
llmService,
|
|
114
212
|
model,
|
|
213
|
+
recursionLimit,
|
|
115
214
|
});
|
|
116
215
|
},
|
|
117
216
|
};
|
|
118
217
|
}
|
|
119
218
|
|
|
120
|
-
|
|
219
|
+
type DispatchRuleEntry = {
|
|
220
|
+
rule: any;
|
|
221
|
+
employee: any;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
type DispatchTaskResult = {
|
|
225
|
+
index: number;
|
|
226
|
+
subAgent: string;
|
|
227
|
+
status: 'success' | 'error';
|
|
228
|
+
content: string;
|
|
229
|
+
durationMs: number;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
function formatDispatchResults(results: DispatchTaskResult[], rulesBySubAgent: Map<string, DispatchRuleEntry>) {
|
|
233
|
+
const total = results.length;
|
|
234
|
+
const ok = results.filter((r) => r.status === 'success').length;
|
|
235
|
+
const lines = [
|
|
236
|
+
`Dispatched ${total} sub-task(s) — ${ok} succeeded, ${
|
|
237
|
+
total - ok
|
|
238
|
+
} failed (max ${MAX_DISPATCH_CONCURRENCY} ran in parallel).`,
|
|
239
|
+
'',
|
|
240
|
+
];
|
|
241
|
+
for (const r of results) {
|
|
242
|
+
const employee = rulesBySubAgent.get(r.subAgent)?.employee;
|
|
243
|
+
const displayName = employee?.nickname || r.subAgent;
|
|
244
|
+
const dur = `${(r.durationMs / 1000).toFixed(1)}s`;
|
|
245
|
+
lines.push(`--- [${r.index + 1}] ${displayName} (${r.subAgent}) [${r.status}] (${dur}) ---`);
|
|
246
|
+
lines.push(r.content || '(empty)');
|
|
247
|
+
lines.push('');
|
|
248
|
+
}
|
|
249
|
+
return lines.join('\n').trimEnd();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Build a single fan-out tool per leader. The leader passes a list of
|
|
254
|
+
* `{ subAgent, task, context? }` items; we run them concurrently (capped at
|
|
255
|
+
* MAX_DISPATCH_CONCURRENCY) and aggregate the results into one response.
|
|
256
|
+
*
|
|
257
|
+
* Each underlying execution still goes through `invokeDelegateTask`, so depth
|
|
258
|
+
* limits, per-rule timeouts, LLM overrides, and orchestratorLogs entries
|
|
259
|
+
* behave identically to a direct `delegate_*_to_*` call.
|
|
260
|
+
*/
|
|
261
|
+
function createDispatchToolOptions(
|
|
262
|
+
plugin: any,
|
|
263
|
+
options: {
|
|
264
|
+
leaderUsername: string;
|
|
265
|
+
rulesBySubAgent: Map<string, DispatchRuleEntry>;
|
|
266
|
+
},
|
|
267
|
+
) {
|
|
268
|
+
const { leaderUsername, rulesBySubAgent } = options;
|
|
269
|
+
const toolName = buildDispatchToolName(leaderUsername);
|
|
270
|
+
const subAgentNames = Array.from(rulesBySubAgent.keys());
|
|
271
|
+
|
|
272
|
+
const subAgentList = subAgentNames
|
|
273
|
+
.map((username) => {
|
|
274
|
+
const entry = rulesBySubAgent.get(username);
|
|
275
|
+
if (!entry) return `- ${username}`;
|
|
276
|
+
const profile = entry.employee?.about ? ` — ${String(entry.employee.about).substring(0, 120)}` : '';
|
|
277
|
+
const display = entry.employee?.nickname ? ` (${entry.employee.nickname})` : '';
|
|
278
|
+
return `- ${username}${display}${profile}`;
|
|
279
|
+
})
|
|
280
|
+
.join('\n');
|
|
281
|
+
|
|
282
|
+
const description = [
|
|
283
|
+
`Dispatch multiple tasks from "${leaderUsername}" to its configured sub-agents in one call.`,
|
|
284
|
+
`At most ${MAX_DISPATCH_CONCURRENCY} sub-tasks run in parallel; up to ${MAX_DISPATCH_TASKS} tasks per call.`,
|
|
285
|
+
'Use this when you have already planned independent sub-tasks and want to fan-out, then aggregate the results.',
|
|
286
|
+
`Available sub-agents:\n${subAgentList}`,
|
|
287
|
+
].join(' ');
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
scope: 'CUSTOM',
|
|
291
|
+
execution: 'backend',
|
|
292
|
+
defaultPermission: 'ALLOW',
|
|
293
|
+
silence: false,
|
|
294
|
+
introduction: {
|
|
295
|
+
title: `[${leaderUsername}] Dispatch sub-agents`,
|
|
296
|
+
about: description,
|
|
297
|
+
},
|
|
298
|
+
definition: {
|
|
299
|
+
name: toolName,
|
|
300
|
+
description,
|
|
301
|
+
schema: z.object({
|
|
302
|
+
tasks: z
|
|
303
|
+
.array(
|
|
304
|
+
z.object({
|
|
305
|
+
subAgent: z
|
|
306
|
+
.enum(subAgentNames as [string, ...string[]])
|
|
307
|
+
.describe('Username of the sub-agent that should execute this task.'),
|
|
308
|
+
task: z.string().describe('Detailed task description for the sub-agent.'),
|
|
309
|
+
context: z.string().optional().describe('Optional additional context for the sub-agent.'),
|
|
310
|
+
}),
|
|
311
|
+
)
|
|
312
|
+
.min(1)
|
|
313
|
+
.max(MAX_DISPATCH_TASKS)
|
|
314
|
+
.describe(`List of sub-tasks to dispatch concurrently. Up to ${MAX_DISPATCH_CONCURRENCY} run in parallel.`),
|
|
315
|
+
}),
|
|
316
|
+
},
|
|
317
|
+
invoke: async (
|
|
318
|
+
ctx: Context,
|
|
319
|
+
args: { tasks: Array<{ subAgent: string; task: string; context?: string }> },
|
|
320
|
+
id: string,
|
|
321
|
+
) => {
|
|
322
|
+
const callingEmployee = await resolveCallingEmployee(ctx, plugin);
|
|
323
|
+
if (!callingEmployee) {
|
|
324
|
+
const distinctSubs = Array.from(new Set((args.tasks ?? []).map((t) => t.subAgent).filter(Boolean)));
|
|
325
|
+
const reportedSub = distinctSubs.length === 1 ? distinctSubs[0] : '(multiple)';
|
|
326
|
+
await logDelegation(ctx, plugin, {
|
|
327
|
+
leaderUsername,
|
|
328
|
+
subAgentUsername: reportedSub,
|
|
329
|
+
toolName,
|
|
330
|
+
task: truncateText(args.tasks ?? [], 2000),
|
|
331
|
+
result: '',
|
|
332
|
+
status: 'error',
|
|
333
|
+
depth: (ctx as any)[ORCHESTRATOR_DEPTH_KEY] ?? 0,
|
|
334
|
+
durationMs: 0,
|
|
335
|
+
error: `Cannot determine calling AI employee for dispatch tool "${toolName}". Targets: ${
|
|
336
|
+
distinctSubs.join(', ') || '(empty)'
|
|
337
|
+
}.`,
|
|
338
|
+
});
|
|
339
|
+
return {
|
|
340
|
+
status: 'error' as const,
|
|
341
|
+
content: `Cannot determine calling AI employee for "${toolName}". Start the request from an AI Employee conversation so leader scoping can be enforced.`,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (callingEmployee && callingEmployee !== leaderUsername) {
|
|
345
|
+
// Mirror the per-rule delegate tool: persist the rejection to
|
|
346
|
+
// orchestratorLogs so admins can investigate via the Tracing tab.
|
|
347
|
+
const distinctSubs = Array.from(new Set((args.tasks ?? []).map((t) => t.subAgent).filter(Boolean)));
|
|
348
|
+
const reportedSub = distinctSubs.length === 1 ? distinctSubs[0] : '(multiple)';
|
|
349
|
+
await logDelegation(ctx, plugin, {
|
|
350
|
+
leaderUsername,
|
|
351
|
+
subAgentUsername: reportedSub,
|
|
352
|
+
toolName,
|
|
353
|
+
task: truncateText(args.tasks ?? [], 2000),
|
|
354
|
+
result: '',
|
|
355
|
+
status: 'error',
|
|
356
|
+
depth: (ctx as any)[ORCHESTRATOR_DEPTH_KEY] ?? 0,
|
|
357
|
+
durationMs: 0,
|
|
358
|
+
error: `Employee "${callingEmployee}" is not authorized to dispatch sub-agents for leader "${leaderUsername}". Targets: ${
|
|
359
|
+
distinctSubs.join(', ') || '(empty)'
|
|
360
|
+
}.`,
|
|
361
|
+
});
|
|
362
|
+
return {
|
|
363
|
+
status: 'error' as const,
|
|
364
|
+
content: `Employee "${callingEmployee}" is not authorized to dispatch sub-agents for leader "${leaderUsername}".`,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const tasks = args.tasks ?? [];
|
|
369
|
+
if (!tasks.length) {
|
|
370
|
+
return {
|
|
371
|
+
status: 'error' as const,
|
|
372
|
+
content: 'No tasks provided. Pass at least one item in `tasks`.',
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const dispatchRootRunId =
|
|
377
|
+
getOrchestratorTraceContext(ctx)?.rootRunId || createRootRunId(`${leaderUsername}:dispatch`);
|
|
378
|
+
|
|
379
|
+
const results = await runWithConcurrency<
|
|
380
|
+
{ subAgent: string; task: string; context?: string },
|
|
381
|
+
DispatchTaskResult
|
|
382
|
+
>(tasks, MAX_DISPATCH_CONCURRENCY, async (item, i) => {
|
|
383
|
+
const startedAt = Date.now();
|
|
384
|
+
const entry = rulesBySubAgent.get(item.subAgent);
|
|
385
|
+
if (!entry) {
|
|
386
|
+
return {
|
|
387
|
+
index: i,
|
|
388
|
+
subAgent: item.subAgent,
|
|
389
|
+
status: 'error',
|
|
390
|
+
content: `Unknown sub-agent "${item.subAgent}". Allowed: ${subAgentNames.join(', ')}.`,
|
|
391
|
+
durationMs: 0,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
const res = await invokeDelegateTask(ctx, plugin, {
|
|
397
|
+
leaderUsername,
|
|
398
|
+
subAgentUsername: item.subAgent,
|
|
399
|
+
subAgentEmployee: entry.employee,
|
|
400
|
+
task: item.task,
|
|
401
|
+
context: item.context,
|
|
402
|
+
maxDepth: entry.rule.maxDepth ?? 1,
|
|
403
|
+
timeout: entry.rule.timeout ?? 120000,
|
|
404
|
+
toolCallId: `${id}-${i}`,
|
|
405
|
+
toolName,
|
|
406
|
+
llmService: entry.rule.llmService,
|
|
407
|
+
model: entry.rule.model,
|
|
408
|
+
recursionLimit: entry.rule.recursionLimit,
|
|
409
|
+
rootRunId: dispatchRootRunId,
|
|
410
|
+
});
|
|
411
|
+
return {
|
|
412
|
+
index: i,
|
|
413
|
+
subAgent: item.subAgent,
|
|
414
|
+
status: res.status,
|
|
415
|
+
content: res.content,
|
|
416
|
+
durationMs: Date.now() - startedAt,
|
|
417
|
+
};
|
|
418
|
+
} catch (e: any) {
|
|
419
|
+
return {
|
|
420
|
+
index: i,
|
|
421
|
+
subAgent: item.subAgent,
|
|
422
|
+
status: 'error',
|
|
423
|
+
content: e?.message || String(e),
|
|
424
|
+
durationMs: Date.now() - startedAt,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const successCount = results.filter((r) => r.status === 'success').length;
|
|
430
|
+
return {
|
|
431
|
+
status: (successCount > 0 ? 'success' : 'error') as 'success' | 'error',
|
|
432
|
+
content: formatDispatchResults(results, rulesBySubAgent),
|
|
433
|
+
};
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
type CtxSnapshot = {
|
|
439
|
+
userId?: number;
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Read the few ctx fields we depend on once, before kicking off the long-running
|
|
444
|
+
* sub-agent. Avoids "ctx is destroyed" or stale-state issues when we later
|
|
445
|
+
* write the orchestratorLogs row from inside the agent's execution promise.
|
|
446
|
+
*/
|
|
447
|
+
function captureCtxSnapshot(ctx: Context): CtxSnapshot {
|
|
448
|
+
let userId: number | undefined;
|
|
449
|
+
try {
|
|
450
|
+
userId = (ctx as any).auth?.user?.id || (ctx as any).state?.currentUser?.id;
|
|
451
|
+
} catch {
|
|
452
|
+
// ctx already disposed — nothing to capture.
|
|
453
|
+
}
|
|
454
|
+
return { userId };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function normalizeEmployeeUsername(raw: any) {
|
|
458
|
+
if (!raw) return null;
|
|
459
|
+
if (typeof raw === 'string') return raw;
|
|
460
|
+
return raw.username || raw.aiEmployeeUsername || raw.name || null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function resolveCallingEmployee(ctx: Context, plugin: any) {
|
|
464
|
+
const values = (ctx as any).action?.params?.values || {};
|
|
121
465
|
const raw =
|
|
122
466
|
(ctx as any)._currentAIEmployee ||
|
|
123
467
|
(ctx as any).state?.currentAIEmployee ||
|
|
124
|
-
(ctx as any).runtime?.context?.currentAIEmployee
|
|
125
|
-
|
|
126
|
-
|
|
468
|
+
(ctx as any).runtime?.context?.currentAIEmployee ||
|
|
469
|
+
values.aiEmployee;
|
|
470
|
+
|
|
471
|
+
const direct = normalizeEmployeeUsername(raw);
|
|
472
|
+
if (direct) return direct;
|
|
473
|
+
|
|
474
|
+
const sessionId = values.sessionId || (ctx as any).action?.params?.sessionId;
|
|
475
|
+
if (!sessionId) return null;
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
const repo = (ctx as any).db?.getRepository?.('aiConversations') || plugin.db.getRepository('aiConversations');
|
|
479
|
+
const conversation = await repo.findOne({
|
|
480
|
+
filter: { sessionId },
|
|
481
|
+
});
|
|
482
|
+
return normalizeEmployeeUsername(conversation?.aiEmployeeUsername || conversation?.get?.('aiEmployeeUsername'));
|
|
483
|
+
} catch (e) {
|
|
484
|
+
plugin.app.log.warn(`[AgentOrchestrator] Failed to resolve AI employee for session "${sessionId}"`, e);
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
127
487
|
}
|
|
128
488
|
|
|
129
489
|
function truncateText(value: any, maxLen: number) {
|
|
@@ -135,6 +495,202 @@ function nowIso() {
|
|
|
135
495
|
return new Date().toISOString();
|
|
136
496
|
}
|
|
137
497
|
|
|
498
|
+
function hasModelSettings(value: any): value is { llmService: string; model: string } {
|
|
499
|
+
return Boolean(value?.llmService && value?.model);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Cache for built delegate tool descriptors to avoid re-querying DB on every
|
|
504
|
+
* core toolsManager.listTools() call (which can fire many times per chat turn).
|
|
505
|
+
*
|
|
506
|
+
* - TTL is a safety net in case event hooks miss an external write path.
|
|
507
|
+
* - DB hooks invalidate immediately on rule/employee changes so admin edits
|
|
508
|
+
* take effect on the next request.
|
|
509
|
+
*/
|
|
510
|
+
const TOOLS_CACHE_TTL_MS = 30_000;
|
|
511
|
+
let toolsCacheByPlugin: WeakMap<object, { tools: any[]; expiresAt: number }> = new WeakMap();
|
|
512
|
+
let hooksAttached: WeakSet<object> | null = null;
|
|
513
|
+
|
|
514
|
+
function attachInvalidationHooks(plugin: any) {
|
|
515
|
+
// Attach once per plugin instance (handles dev hot-reload safely).
|
|
516
|
+
if (!hooksAttached) hooksAttached = new WeakSet<object>();
|
|
517
|
+
if (hooksAttached.has(plugin)) return;
|
|
518
|
+
hooksAttached.add(plugin);
|
|
519
|
+
|
|
520
|
+
const invalidate = () => {
|
|
521
|
+
toolsCacheByPlugin.delete(plugin);
|
|
522
|
+
registeredDelegateNamesByPlugin.delete(plugin);
|
|
523
|
+
};
|
|
524
|
+
plugin.db.on('orchestratorConfig.afterCreate', invalidate);
|
|
525
|
+
plugin.db.on('orchestratorConfig.afterUpdate', invalidate);
|
|
526
|
+
plugin.db.on('orchestratorConfig.afterDestroy', invalidate);
|
|
527
|
+
plugin.db.on('aiEmployees.afterCreate', invalidate);
|
|
528
|
+
plugin.db.on('aiEmployees.afterUpdate', invalidate);
|
|
529
|
+
plugin.db.on('aiEmployees.afterDestroy', invalidate);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function buildDelegateTools(plugin: any) {
|
|
533
|
+
const configRepo = plugin.db.getRepository('orchestratorConfig');
|
|
534
|
+
if (!configRepo) {
|
|
535
|
+
registeredDelegateNamesByPlugin.set(plugin, new Set());
|
|
536
|
+
return [];
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const configs = await configRepo.find({
|
|
540
|
+
filter: { enabled: true },
|
|
541
|
+
});
|
|
542
|
+
if (!configs?.length) {
|
|
543
|
+
registeredDelegateNamesByPlugin.set(plugin, new Set());
|
|
544
|
+
return [];
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const employeeCache = new Map<string, any>();
|
|
548
|
+
const tools: any[] = [];
|
|
549
|
+
// Track every generated tool name to surface sanitize() collisions
|
|
550
|
+
// (e.g. "pm-1" and "pm.1" both → "pm_1"). Collisions are skipped + logged.
|
|
551
|
+
const generatedNames = new Map<string, { leader: string; sub: string }>();
|
|
552
|
+
const configsBySubAgent = new Map<string, any[]>();
|
|
553
|
+
for (const config of configs) {
|
|
554
|
+
const items = configsBySubAgent.get(config.subAgentUsername) || [];
|
|
555
|
+
items.push(config);
|
|
556
|
+
configsBySubAgent.set(config.subAgentUsername, items);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
for (const config of configs) {
|
|
560
|
+
const { leaderUsername, subAgentUsername, maxDepth, timeout, recursionLimit } = config;
|
|
561
|
+
|
|
562
|
+
let subAgentEmployee = employeeCache.get(subAgentUsername);
|
|
563
|
+
if (!subAgentEmployee) {
|
|
564
|
+
subAgentEmployee = await plugin.db.getRepository('aiEmployees').findOne({
|
|
565
|
+
filter: { username: subAgentUsername },
|
|
566
|
+
});
|
|
567
|
+
if (subAgentEmployee) {
|
|
568
|
+
employeeCache.set(subAgentUsername, subAgentEmployee);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (!subAgentEmployee) continue;
|
|
572
|
+
|
|
573
|
+
const toolName = buildDelegateToolName(leaderUsername, subAgentUsername);
|
|
574
|
+
if (toolName.length > MAX_TOOL_NAME_LENGTH) {
|
|
575
|
+
plugin.app.log.error(
|
|
576
|
+
`[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.`,
|
|
577
|
+
);
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
const existing = generatedNames.get(toolName);
|
|
581
|
+
if (existing) {
|
|
582
|
+
const suffix = createHash('sha1').update(`${leaderUsername}::${subAgentUsername}`).digest('hex').slice(0, 6);
|
|
583
|
+
plugin.app.log.error(
|
|
584
|
+
`[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.`,
|
|
585
|
+
);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
generatedNames.set(toolName, { leader: leaderUsername, sub: subAgentUsername });
|
|
589
|
+
tools.push(
|
|
590
|
+
createDelegateToolOptions(plugin, {
|
|
591
|
+
leaderUsername,
|
|
592
|
+
subAgentUsername,
|
|
593
|
+
subAgentEmployee,
|
|
594
|
+
maxDepth,
|
|
595
|
+
timeout,
|
|
596
|
+
toolName,
|
|
597
|
+
llmService: config.llmService,
|
|
598
|
+
model: config.model,
|
|
599
|
+
recursionLimit,
|
|
600
|
+
}),
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Compatibility for existing single-parent setups that already assigned
|
|
605
|
+
// delegate_to_<sub> to the parent employee's skills.
|
|
606
|
+
for (const [subAgentUsername, items] of configsBySubAgent.entries()) {
|
|
607
|
+
if (items.length !== 1) {
|
|
608
|
+
// Multiple leaders for the same sub-agent ⇒ alias is ambiguous.
|
|
609
|
+
// Surface it so admins know why old skill assignments may stop working.
|
|
610
|
+
const leaders = items.map((c: any) => c.leaderUsername).join(', ');
|
|
611
|
+
plugin.app.log.warn(
|
|
612
|
+
`[AgentOrchestrator] Legacy alias "delegate_to_${sanitizeToolPart(
|
|
613
|
+
subAgentUsername,
|
|
614
|
+
)}" 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.`,
|
|
615
|
+
);
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
const config = items[0];
|
|
619
|
+
const subAgentEmployee = employeeCache.get(subAgentUsername);
|
|
620
|
+
if (!subAgentEmployee) continue;
|
|
621
|
+
const legacyToolName = `delegate_to_${sanitizeToolPart(subAgentUsername)}`;
|
|
622
|
+
if (legacyToolName.length > MAX_TOOL_NAME_LENGTH) {
|
|
623
|
+
plugin.app.log.error(
|
|
624
|
+
`[AgentOrchestrator] Legacy alias "${legacyToolName}" exceeds the ${MAX_TOOL_NAME_LENGTH}-char limit. Skipping alias for sub-agent "${subAgentUsername}".`,
|
|
625
|
+
);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
const aliasExisting = generatedNames.get(legacyToolName);
|
|
629
|
+
if (aliasExisting) {
|
|
630
|
+
plugin.app.log.error(
|
|
631
|
+
`[AgentOrchestrator] Legacy alias "${legacyToolName}" collides with another rule (${aliasExisting.leader} → ${aliasExisting.sub}). Skipping alias registration.`,
|
|
632
|
+
);
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
generatedNames.set(legacyToolName, {
|
|
636
|
+
leader: config.leaderUsername,
|
|
637
|
+
sub: subAgentUsername,
|
|
638
|
+
});
|
|
639
|
+
tools.push(
|
|
640
|
+
createDelegateToolOptions(plugin, {
|
|
641
|
+
leaderUsername: config.leaderUsername,
|
|
642
|
+
subAgentUsername,
|
|
643
|
+
subAgentEmployee,
|
|
644
|
+
maxDepth: config.maxDepth,
|
|
645
|
+
timeout: config.timeout,
|
|
646
|
+
toolName: legacyToolName,
|
|
647
|
+
legacyAlias: true,
|
|
648
|
+
llmService: config.llmService,
|
|
649
|
+
model: config.model,
|
|
650
|
+
recursionLimit: config.recursionLimit,
|
|
651
|
+
}),
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// One dispatch fan-out tool per leader.
|
|
656
|
+
const rulesByLeader = new Map<string, Map<string, DispatchRuleEntry>>();
|
|
657
|
+
for (const config of configs) {
|
|
658
|
+
const subAgentEmployee = employeeCache.get(config.subAgentUsername);
|
|
659
|
+
if (!subAgentEmployee) continue;
|
|
660
|
+
let bucket = rulesByLeader.get(config.leaderUsername);
|
|
661
|
+
if (!bucket) {
|
|
662
|
+
bucket = new Map<string, DispatchRuleEntry>();
|
|
663
|
+
rulesByLeader.set(config.leaderUsername, bucket);
|
|
664
|
+
}
|
|
665
|
+
bucket.set(config.subAgentUsername, { rule: config, employee: subAgentEmployee });
|
|
666
|
+
}
|
|
667
|
+
for (const [leaderUsername, rulesBySubAgent] of rulesByLeader.entries()) {
|
|
668
|
+
if (!rulesBySubAgent.size) continue;
|
|
669
|
+
const dispatchToolName = buildDispatchToolName(leaderUsername);
|
|
670
|
+
if (dispatchToolName.length > MAX_TOOL_NAME_LENGTH) {
|
|
671
|
+
plugin.app.log.error(
|
|
672
|
+
`[AgentOrchestrator] Dispatch tool "${dispatchToolName}" exceeds the ${MAX_TOOL_NAME_LENGTH}-char limit. Skipping for leader "${leaderUsername}".`,
|
|
673
|
+
);
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
const dispatchExisting = generatedNames.get(dispatchToolName);
|
|
677
|
+
if (dispatchExisting) {
|
|
678
|
+
plugin.app.log.error(
|
|
679
|
+
`[AgentOrchestrator] Dispatch tool "${dispatchToolName}" collides with another generated tool (${dispatchExisting.leader} → ${dispatchExisting.sub}). Skipping dispatch registration for leader "${leaderUsername}".`,
|
|
680
|
+
);
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
generatedNames.set(dispatchToolName, { leader: leaderUsername, sub: '(dispatch)' });
|
|
684
|
+
tools.push(createDispatchToolOptions(plugin, { leaderUsername, rulesBySubAgent }));
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Refresh the registry that `isDelegateToolName` consults so sub-agents
|
|
688
|
+
// running concurrently filter exactly the names we just registered.
|
|
689
|
+
registeredDelegateNamesByPlugin.set(plugin, new Set(generatedNames.keys()));
|
|
690
|
+
|
|
691
|
+
return tools;
|
|
692
|
+
}
|
|
693
|
+
|
|
138
694
|
/**
|
|
139
695
|
* Creates one dynamic tool per configured sub-agent for a given leader.
|
|
140
696
|
* Uses Strategy B (Per-SubAgent Tool): each sub-agent becomes a separate tool
|
|
@@ -150,77 +706,19 @@ function nowIso() {
|
|
|
150
706
|
* leaderUsername field, so scoping is enforced in the invoke callback)
|
|
151
707
|
*/
|
|
152
708
|
export function createDelegateToolsProvider(plugin: any) {
|
|
709
|
+
attachInvalidationHooks(plugin);
|
|
710
|
+
|
|
153
711
|
return async (register: any) => {
|
|
154
712
|
try {
|
|
155
|
-
|
|
156
|
-
if (!
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
});
|
|
161
|
-
if (!configs?.length) return;
|
|
162
|
-
|
|
163
|
-
const employeeCache = new Map<string, any>();
|
|
164
|
-
const tools = [];
|
|
165
|
-
const configsBySubAgent = new Map<string, any[]>();
|
|
166
|
-
for (const config of configs) {
|
|
167
|
-
const items = configsBySubAgent.get(config.subAgentUsername) || [];
|
|
168
|
-
items.push(config);
|
|
169
|
-
configsBySubAgent.set(config.subAgentUsername, items);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
for (const config of configs) {
|
|
173
|
-
const { leaderUsername, subAgentUsername, maxDepth, timeout } = config;
|
|
174
|
-
|
|
175
|
-
// Fetch the sub-agent employee model for its description and LLM config
|
|
176
|
-
let subAgentEmployee = employeeCache.get(subAgentUsername);
|
|
177
|
-
if (!subAgentEmployee) {
|
|
178
|
-
subAgentEmployee = await plugin.db.getRepository('aiEmployees').findOne({
|
|
179
|
-
filter: { username: subAgentUsername },
|
|
180
|
-
});
|
|
181
|
-
if (subAgentEmployee) {
|
|
182
|
-
employeeCache.set(subAgentUsername, subAgentEmployee);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
if (!subAgentEmployee) continue;
|
|
186
|
-
|
|
187
|
-
const toolName = buildDelegateToolName(leaderUsername, subAgentUsername);
|
|
188
|
-
tools.push(createDelegateToolOptions(plugin, {
|
|
189
|
-
leaderUsername,
|
|
190
|
-
subAgentUsername,
|
|
191
|
-
subAgentEmployee,
|
|
192
|
-
maxDepth,
|
|
193
|
-
timeout,
|
|
194
|
-
toolName,
|
|
195
|
-
llmService: config.llmService,
|
|
196
|
-
model: config.model,
|
|
197
|
-
}));
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Compatibility for existing single-parent setups that already assigned
|
|
201
|
-
// delegate_to_<sub> to the parent employee's skills.
|
|
202
|
-
for (const [subAgentUsername, items] of configsBySubAgent.entries()) {
|
|
203
|
-
if (items.length !== 1) continue;
|
|
204
|
-
const config = items[0];
|
|
205
|
-
const subAgentEmployee = employeeCache.get(subAgentUsername);
|
|
206
|
-
if (!subAgentEmployee) continue;
|
|
207
|
-
const legacyToolName = `delegate_to_${sanitizeToolPart(subAgentUsername)}`;
|
|
208
|
-
if (tools.some((tool: any) => tool.definition.name === legacyToolName)) continue;
|
|
209
|
-
tools.push(createDelegateToolOptions(plugin, {
|
|
210
|
-
leaderUsername: config.leaderUsername,
|
|
211
|
-
subAgentUsername,
|
|
212
|
-
subAgentEmployee,
|
|
213
|
-
maxDepth: config.maxDepth,
|
|
214
|
-
timeout: config.timeout,
|
|
215
|
-
toolName: legacyToolName,
|
|
216
|
-
legacyAlias: true,
|
|
217
|
-
llmService: config.llmService,
|
|
218
|
-
model: config.model,
|
|
219
|
-
}));
|
|
713
|
+
let toolsCache = toolsCacheByPlugin.get(plugin);
|
|
714
|
+
if (!toolsCache || toolsCache.expiresAt <= Date.now()) {
|
|
715
|
+
const tools = await buildDelegateTools(plugin);
|
|
716
|
+
toolsCache = { tools, expiresAt: Date.now() + TOOLS_CACHE_TTL_MS };
|
|
717
|
+
toolsCacheByPlugin.set(plugin, toolsCache);
|
|
220
718
|
}
|
|
221
719
|
|
|
222
|
-
if (tools.length) {
|
|
223
|
-
register.registerTools(tools);
|
|
720
|
+
if (toolsCache.tools.length) {
|
|
721
|
+
register.registerTools(toolsCache.tools);
|
|
224
722
|
}
|
|
225
723
|
} catch (e) {
|
|
226
724
|
plugin.app.log.error('[AgentOrchestrator] Failed to register delegate tools', e);
|
|
@@ -228,6 +726,14 @@ export function createDelegateToolsProvider(plugin: any) {
|
|
|
228
726
|
};
|
|
229
727
|
}
|
|
230
728
|
|
|
729
|
+
/**
|
|
730
|
+
* Test/internal helper to drop the in-memory tool cache (e.g., from a CLI op).
|
|
731
|
+
*/
|
|
732
|
+
export function invalidateDelegateToolsCache() {
|
|
733
|
+
toolsCacheByPlugin = new WeakMap();
|
|
734
|
+
registeredDelegateNamesByPlugin = new WeakMap();
|
|
735
|
+
}
|
|
736
|
+
|
|
231
737
|
/**
|
|
232
738
|
* Core execution logic using createReactAgent (public LangGraph API).
|
|
233
739
|
*
|
|
@@ -259,9 +765,34 @@ async function invokeDelegateTask(
|
|
|
259
765
|
toolName: string;
|
|
260
766
|
llmService?: string;
|
|
261
767
|
model?: string;
|
|
768
|
+
recursionLimit?: number;
|
|
769
|
+
rootRunId?: string;
|
|
770
|
+
parentSpanId?: string;
|
|
262
771
|
},
|
|
263
772
|
) {
|
|
264
|
-
const {
|
|
773
|
+
const {
|
|
774
|
+
leaderUsername,
|
|
775
|
+
subAgentUsername,
|
|
776
|
+
subAgentEmployee,
|
|
777
|
+
task,
|
|
778
|
+
context,
|
|
779
|
+
maxDepth,
|
|
780
|
+
timeout,
|
|
781
|
+
toolCallId,
|
|
782
|
+
toolName,
|
|
783
|
+
llmService,
|
|
784
|
+
model,
|
|
785
|
+
recursionLimit,
|
|
786
|
+
rootRunId: providedRootRunId,
|
|
787
|
+
parentSpanId: providedParentSpanId,
|
|
788
|
+
} = options;
|
|
789
|
+
|
|
790
|
+
// --- Snapshot ctx fields up-front ---
|
|
791
|
+
// Long-running agent execution (up to `timeout` ms) outlives the parent HTTP
|
|
792
|
+
// request, so middleware may have cleared `ctx.auth`, `ctx.state`, or even
|
|
793
|
+
// disposed the underlying socket by the time we finalize the log row.
|
|
794
|
+
// Capturing the values once here keeps log/audit fields stable.
|
|
795
|
+
const ctxSnapshot = captureCtxSnapshot(ctx);
|
|
265
796
|
|
|
266
797
|
// --- P1: Depth enforcement ---
|
|
267
798
|
const currentDepth: number = (ctx as any)[ORCHESTRATOR_DEPTH_KEY] ?? 0;
|
|
@@ -277,6 +808,7 @@ async function invokeDelegateTask(
|
|
|
277
808
|
depth: currentDepth,
|
|
278
809
|
durationMs: 0,
|
|
279
810
|
error: `Delegation depth limit reached (${currentDepth}/${maxDepth}).`,
|
|
811
|
+
snapshot: ctxSnapshot,
|
|
280
812
|
});
|
|
281
813
|
return {
|
|
282
814
|
status: 'error' as const,
|
|
@@ -284,6 +816,11 @@ async function invokeDelegateTask(
|
|
|
284
816
|
};
|
|
285
817
|
}
|
|
286
818
|
|
|
819
|
+
const spanService = new ExecutionSpanService(plugin);
|
|
820
|
+
const upstreamTraceContext = getOrchestratorTraceContext(ctx);
|
|
821
|
+
const rootRunId =
|
|
822
|
+
providedRootRunId || upstreamTraceContext?.rootRunId || createRootRunId(`${leaderUsername}:${subAgentUsername}`);
|
|
823
|
+
const parentSpanId = providedParentSpanId || upstreamTraceContext?.spanId || upstreamTraceContext?.parentSpanId;
|
|
287
824
|
const startTime = Date.now();
|
|
288
825
|
const trace: TraceEvent[] = [
|
|
289
826
|
{
|
|
@@ -293,6 +830,25 @@ async function invokeDelegateTask(
|
|
|
293
830
|
content: task,
|
|
294
831
|
},
|
|
295
832
|
];
|
|
833
|
+
const executionSpan = await spanService.create({
|
|
834
|
+
rootRunId,
|
|
835
|
+
parentSpanId,
|
|
836
|
+
type: 'sub_agent',
|
|
837
|
+
status: 'running',
|
|
838
|
+
leaderUsername,
|
|
839
|
+
employeeUsername: subAgentUsername,
|
|
840
|
+
title: `Delegation: ${leaderUsername} -> ${subAgentUsername}`,
|
|
841
|
+
input: { task, context },
|
|
842
|
+
metadata: {
|
|
843
|
+
depth: currentDepth,
|
|
844
|
+
maxDepth,
|
|
845
|
+
toolName,
|
|
846
|
+
recursionLimit,
|
|
847
|
+
llmOverride: llmService && model ? { llmService, model } : undefined,
|
|
848
|
+
},
|
|
849
|
+
userId: ctxSnapshot.userId,
|
|
850
|
+
});
|
|
851
|
+
const executionSpanId = executionSpan?.id ? String(executionSpan.id) : undefined;
|
|
296
852
|
const logRecord = await logDelegation(ctx, plugin, {
|
|
297
853
|
leaderUsername,
|
|
298
854
|
subAgentUsername,
|
|
@@ -304,7 +860,11 @@ async function invokeDelegateTask(
|
|
|
304
860
|
depth: currentDepth,
|
|
305
861
|
durationMs: 0,
|
|
306
862
|
trace,
|
|
863
|
+
snapshot: ctxSnapshot,
|
|
307
864
|
});
|
|
865
|
+
if (executionSpanId && logRecord?.id) {
|
|
866
|
+
await spanService.update(executionSpanId, { orchestratorLogId: logRecord.id });
|
|
867
|
+
}
|
|
308
868
|
|
|
309
869
|
try {
|
|
310
870
|
const aiPlugin = ctx.app.pm.get('ai') as PluginAIServer;
|
|
@@ -313,26 +873,32 @@ async function invokeDelegateTask(
|
|
|
313
873
|
}
|
|
314
874
|
|
|
315
875
|
// --- Step 1: Resolve LLM model from sub-agent's employee config ---
|
|
316
|
-
let modelSettings = subAgentEmployee.modelSettings;
|
|
317
|
-
|
|
876
|
+
let modelSettings = hasModelSettings(subAgentEmployee.modelSettings) ? subAgentEmployee.modelSettings : undefined;
|
|
877
|
+
|
|
318
878
|
// Override with orchestrator config if provided
|
|
319
879
|
if (llmService && model) {
|
|
320
880
|
modelSettings = { llmService, model };
|
|
321
881
|
}
|
|
322
|
-
|
|
323
|
-
if (!modelSettings
|
|
882
|
+
|
|
883
|
+
if (!hasModelSettings(modelSettings)) {
|
|
324
884
|
// Fallback to leader's LLM model if sub-agent doesn't have one
|
|
325
885
|
const leaderEmployee = await plugin.db.getRepository('aiEmployees').findOne({
|
|
326
886
|
filter: { username: leaderUsername },
|
|
327
887
|
});
|
|
328
|
-
|
|
888
|
+
|
|
329
889
|
// The leader's model might be empty in the DB if it relies on the dynamic system default.
|
|
330
890
|
// In that case, we extract the dynamic `model` passed from the frontend request.
|
|
331
891
|
const dynamicModel = ctx.action?.params?.values?.model;
|
|
332
|
-
modelSettings = leaderEmployee?.modelSettings
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
892
|
+
modelSettings = hasModelSettings(leaderEmployee?.modelSettings)
|
|
893
|
+
? leaderEmployee.modelSettings
|
|
894
|
+
: hasModelSettings(dynamicModel)
|
|
895
|
+
? dynamicModel
|
|
896
|
+
: undefined;
|
|
897
|
+
|
|
898
|
+
if (!hasModelSettings(modelSettings)) {
|
|
899
|
+
throw new Error(
|
|
900
|
+
`Sub-agent "${subAgentUsername}" has no LLM model configured (and leader fallback failed). Please configure a model in the Orchestrator Config or AI Employee settings.`,
|
|
901
|
+
);
|
|
336
902
|
}
|
|
337
903
|
}
|
|
338
904
|
|
|
@@ -350,32 +916,77 @@ async function invokeDelegateTask(
|
|
|
350
916
|
|
|
351
917
|
// skillSettings.skills is { name: string, autoCall: boolean }[]
|
|
352
918
|
// (verified at ai-employee.ts:1028-1029)
|
|
353
|
-
const employeeSkills:
|
|
354
|
-
.map((s: any) =>
|
|
355
|
-
|
|
919
|
+
const employeeSkills: EmployeeSkillConfig[] = (subAgentEmployee.skillSettings?.skills ?? [])
|
|
920
|
+
.map((s: any) =>
|
|
921
|
+
typeof s === 'string'
|
|
922
|
+
? { name: s, autoCall: false }
|
|
923
|
+
: { name: s?.name, autoCall: s?.autoCall === true },
|
|
924
|
+
)
|
|
925
|
+
.filter((s: EmployeeSkillConfig) => Boolean(s.name));
|
|
926
|
+
const employeeSkillMap = new Map(employeeSkills.map((skill) => [skill.name, skill]));
|
|
356
927
|
|
|
357
928
|
const langchainTools: DynamicStructuredTool[] = [];
|
|
358
929
|
|
|
359
930
|
for (const toolEntry of allTools) {
|
|
360
|
-
const
|
|
361
|
-
if (!
|
|
931
|
+
const entryName = toolEntry.definition.name;
|
|
932
|
+
if (!entryName) continue;
|
|
933
|
+
const employeeSkill = employeeSkillMap.get(entryName);
|
|
362
934
|
|
|
363
935
|
// Only include tools that the sub-agent employee is configured to use.
|
|
364
|
-
// Also skip our own
|
|
936
|
+
// Also skip our own orchestration tools to prevent circular delegation
|
|
365
937
|
// (belt-and-suspenders with the depth check above).
|
|
366
|
-
|
|
938
|
+
//
|
|
939
|
+
// Headless sub-agent execution has no human confirmation surface, so we
|
|
940
|
+
// require both the employee assignment and the tool definition to be
|
|
941
|
+
// explicitly auto-callable. This prevents ASK/interactionSchema Skill Hub
|
|
942
|
+
// tools from being executed silently by a delegated sub-agent.
|
|
943
|
+
if (
|
|
944
|
+
!employeeSkill ||
|
|
945
|
+
isDelegateToolName(plugin, entryName) ||
|
|
946
|
+
employeeSkill.autoCall !== true ||
|
|
947
|
+
toolEntry.defaultPermission !== 'ALLOW'
|
|
948
|
+
) {
|
|
367
949
|
continue;
|
|
368
950
|
}
|
|
369
951
|
|
|
370
952
|
langchainTools.push(
|
|
371
953
|
new DynamicStructuredTool({
|
|
372
|
-
name:
|
|
373
|
-
description: toolEntry.definition.description ||
|
|
954
|
+
name: entryName.replace(/[^a-zA-Z0-9_-]/g, '_'),
|
|
955
|
+
description: toolEntry.definition.description || entryName,
|
|
374
956
|
schema: (toolEntry.definition.schema || z.object({})) as any,
|
|
375
957
|
func: async (toolArgs) => {
|
|
376
958
|
// Forward the invoke with depth tracking
|
|
377
959
|
const invokeCtx = Object.create(ctx);
|
|
378
960
|
(invokeCtx as any)[ORCHESTRATOR_DEPTH_KEY] = currentDepth + 1;
|
|
961
|
+
const toolStartedAt = Date.now();
|
|
962
|
+
const isSkillHubTool = entryName === 'skill_hub_execute' || entryName.startsWith('skill_hub_');
|
|
963
|
+
const toolSpan = await spanService.create({
|
|
964
|
+
rootRunId,
|
|
965
|
+
parentSpanId: executionSpanId,
|
|
966
|
+
type: isSkillHubTool ? 'skill' : 'tool',
|
|
967
|
+
status: 'running',
|
|
968
|
+
leaderUsername,
|
|
969
|
+
employeeUsername: subAgentUsername,
|
|
970
|
+
toolName: toolEntry.definition.name,
|
|
971
|
+
title: isSkillHubTool ? `Skill: ${toolEntry.definition.name}` : `Tool: ${toolEntry.definition.name}`,
|
|
972
|
+
input: toolArgs,
|
|
973
|
+
metadata: {
|
|
974
|
+
depth: currentDepth + 1,
|
|
975
|
+
toolCallId: `orch-${toolCallId}`,
|
|
976
|
+
defaultPermission: toolEntry.defaultPermission,
|
|
977
|
+
},
|
|
978
|
+
userId: ctxSnapshot.userId,
|
|
979
|
+
});
|
|
980
|
+
const toolSpanId = toolSpan?.id ? String(toolSpan.id) : undefined;
|
|
981
|
+
setOrchestratorTraceContext(invokeCtx, {
|
|
982
|
+
rootRunId,
|
|
983
|
+
spanId: toolSpanId,
|
|
984
|
+
parentSpanId: executionSpanId,
|
|
985
|
+
toolCallId: `orch-${toolCallId}`,
|
|
986
|
+
leaderUsername,
|
|
987
|
+
employeeUsername: subAgentUsername,
|
|
988
|
+
toolName: toolEntry.definition.name,
|
|
989
|
+
});
|
|
379
990
|
|
|
380
991
|
trace.push({
|
|
381
992
|
type: 'tool_call',
|
|
@@ -387,17 +998,26 @@ async function invokeDelegateTask(
|
|
|
387
998
|
|
|
388
999
|
try {
|
|
389
1000
|
const res = await toolEntry.invoke(invokeCtx, toolArgs, `orch-${toolCallId}`);
|
|
1001
|
+
const output = truncateText(res?.content ?? res?.result ?? res, 50000);
|
|
390
1002
|
trace.push({
|
|
391
1003
|
type: 'tool_result',
|
|
392
1004
|
at: nowIso(),
|
|
393
1005
|
title: `Tool finished: ${toolEntry.definition.name}`,
|
|
394
1006
|
toolName: toolEntry.definition.name,
|
|
395
1007
|
status: res?.status || 'success',
|
|
396
|
-
content: truncateText(
|
|
1008
|
+
content: truncateText(output, 2000),
|
|
397
1009
|
});
|
|
398
1010
|
if (res?.status === 'error') {
|
|
1011
|
+
await spanService.finish(toolSpanId, 'error', toolStartedAt, {
|
|
1012
|
+
output,
|
|
1013
|
+
error: truncateText(res.content || output, 10000),
|
|
1014
|
+
});
|
|
399
1015
|
throw new Error(`Tool <${toolEntry.definition.name}> failed: ${res.content}`);
|
|
400
1016
|
}
|
|
1017
|
+
await spanService.finish(toolSpanId, 'success', toolStartedAt, {
|
|
1018
|
+
output,
|
|
1019
|
+
skillExecutionId: res?.result?.execId || res?.execId,
|
|
1020
|
+
});
|
|
401
1021
|
return typeof res?.content === 'string' ? res.content : JSON.stringify(res);
|
|
402
1022
|
} catch (e: any) {
|
|
403
1023
|
trace.push({
|
|
@@ -408,6 +1028,9 @@ async function invokeDelegateTask(
|
|
|
408
1028
|
status: 'error',
|
|
409
1029
|
content: e.message,
|
|
410
1030
|
});
|
|
1031
|
+
await spanService.finish(toolSpanId, 'error', toolStartedAt, {
|
|
1032
|
+
error: truncateText(e.message, 10000),
|
|
1033
|
+
});
|
|
411
1034
|
throw e;
|
|
412
1035
|
}
|
|
413
1036
|
},
|
|
@@ -423,23 +1046,36 @@ async function invokeDelegateTask(
|
|
|
423
1046
|
});
|
|
424
1047
|
|
|
425
1048
|
// --- Step 4: Construct messages ---
|
|
426
|
-
const systemPrompt =
|
|
427
|
-
|
|
428
|
-
|
|
1049
|
+
const systemPrompt =
|
|
1050
|
+
subAgentEmployee.chatSettings?.systemPrompt ||
|
|
1051
|
+
subAgentEmployee.bio ||
|
|
1052
|
+
`You are an AI assistant named "${subAgentEmployee.nickname || subAgentUsername}". ${
|
|
1053
|
+
subAgentEmployee.about || ''
|
|
1054
|
+
}`;
|
|
429
1055
|
|
|
430
|
-
const combinedTask = context
|
|
431
|
-
? `Task: ${task}\n\nContext Provided:\n${context}`
|
|
432
|
-
: `Task: ${task}`;
|
|
1056
|
+
const combinedTask = context ? `Task: ${task}\n\nContext Provided:\n${context}` : `Task: ${task}`;
|
|
433
1057
|
|
|
434
1058
|
// --- Step 5: Execute with timeout + abort ---
|
|
435
1059
|
// P3 FIX: AbortController signal cancels the in-flight stream on timeout,
|
|
436
1060
|
// preventing continued token consumption after the timeout fires.
|
|
437
|
-
const
|
|
1061
|
+
const effectiveRecursionLimit =
|
|
1062
|
+
Number.isFinite(recursionLimit) && (recursionLimit as number) > 0 ? (recursionLimit as number) : 50;
|
|
1063
|
+
const invokePromise = executeAgent(
|
|
1064
|
+
executor,
|
|
1065
|
+
systemPrompt,
|
|
1066
|
+
combinedTask,
|
|
1067
|
+
abortController.signal,
|
|
1068
|
+
effectiveRecursionLimit,
|
|
1069
|
+
);
|
|
438
1070
|
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
1071
|
+
const timeoutHandle = createTimeout(timeout, subAgentUsername, abortController);
|
|
1072
|
+
let result: AgentExecutionResult;
|
|
1073
|
+
try {
|
|
1074
|
+
result = (await Promise.race([invokePromise, timeoutHandle.promise])) as AgentExecutionResult;
|
|
1075
|
+
} finally {
|
|
1076
|
+
// Always release the timer so it doesn't keep the event loop alive.
|
|
1077
|
+
timeoutHandle.cancel();
|
|
1078
|
+
}
|
|
443
1079
|
|
|
444
1080
|
const content = result.content || 'Sub-agent completed the task but produced no output.';
|
|
445
1081
|
trace.push({
|
|
@@ -464,6 +1100,18 @@ async function invokeDelegateTask(
|
|
|
464
1100
|
durationMs: Date.now() - startTime,
|
|
465
1101
|
trace,
|
|
466
1102
|
messages: result.messages,
|
|
1103
|
+
snapshot: ctxSnapshot,
|
|
1104
|
+
});
|
|
1105
|
+
await spanService.finish(executionSpanId, 'success', startTime, {
|
|
1106
|
+
output: content,
|
|
1107
|
+
metadata: {
|
|
1108
|
+
depth: currentDepth,
|
|
1109
|
+
maxDepth,
|
|
1110
|
+
toolName,
|
|
1111
|
+
recursionLimit,
|
|
1112
|
+
messages: result.messages,
|
|
1113
|
+
traceCount: trace.length,
|
|
1114
|
+
},
|
|
467
1115
|
});
|
|
468
1116
|
|
|
469
1117
|
return {
|
|
@@ -496,9 +1144,20 @@ async function invokeDelegateTask(
|
|
|
496
1144
|
content: e.message,
|
|
497
1145
|
},
|
|
498
1146
|
],
|
|
1147
|
+
snapshot: ctxSnapshot,
|
|
499
1148
|
}).catch((logErr) => {
|
|
500
1149
|
plugin.app.log.warn('[AgentOrchestrator] Failed to save error log for delegation', logErr);
|
|
501
1150
|
});
|
|
1151
|
+
await spanService.finish(executionSpanId, 'error', startTime, {
|
|
1152
|
+
error: truncateText(e.message, 10000),
|
|
1153
|
+
metadata: {
|
|
1154
|
+
depth: currentDepth,
|
|
1155
|
+
maxDepth,
|
|
1156
|
+
toolName,
|
|
1157
|
+
recursionLimit,
|
|
1158
|
+
traceCount: trace.length + 1,
|
|
1159
|
+
},
|
|
1160
|
+
});
|
|
502
1161
|
|
|
503
1162
|
return {
|
|
504
1163
|
status: 'error' as const,
|
|
@@ -527,6 +1186,7 @@ async function logDelegation(
|
|
|
527
1186
|
error?: string;
|
|
528
1187
|
trace?: TraceEvent[];
|
|
529
1188
|
messages?: any[];
|
|
1189
|
+
snapshot?: CtxSnapshot;
|
|
530
1190
|
},
|
|
531
1191
|
) {
|
|
532
1192
|
try {
|
|
@@ -536,12 +1196,16 @@ async function logDelegation(
|
|
|
536
1196
|
return;
|
|
537
1197
|
}
|
|
538
1198
|
|
|
539
|
-
//
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
1199
|
+
// Prefer the early snapshot captured in invokeDelegateTask — by the time
|
|
1200
|
+
// the agent finishes, ctx may already be disposed. Fall back to ctx for
|
|
1201
|
+
// call sites that don't pass a snapshot (e.g. authz-failure short-circuit).
|
|
1202
|
+
let userId: number | undefined = data.snapshot?.userId;
|
|
1203
|
+
if (userId == null) {
|
|
1204
|
+
try {
|
|
1205
|
+
userId = ctx.auth?.user?.id || ctx.state?.currentUser?.id;
|
|
1206
|
+
} catch {
|
|
1207
|
+
// ctx lifecycle ended — proceed without userId
|
|
1208
|
+
}
|
|
545
1209
|
}
|
|
546
1210
|
|
|
547
1211
|
const values = {
|
|
@@ -591,8 +1255,9 @@ async function executeAgent(
|
|
|
591
1255
|
systemPrompt: string,
|
|
592
1256
|
task: string,
|
|
593
1257
|
signal?: AbortSignal,
|
|
1258
|
+
recursionLimit = 50,
|
|
594
1259
|
): Promise<AgentExecutionResult> {
|
|
595
|
-
const config: any = { recursionLimit
|
|
1260
|
+
const config: any = { recursionLimit };
|
|
596
1261
|
if (signal) {
|
|
597
1262
|
config.signal = signal;
|
|
598
1263
|
}
|
|
@@ -606,9 +1271,9 @@ async function executeAgent(
|
|
|
606
1271
|
|
|
607
1272
|
// finalState.messages contains the entire conversation history of this delegation
|
|
608
1273
|
const messages = finalState?.messages || [];
|
|
609
|
-
|
|
1274
|
+
|
|
610
1275
|
// Find the last AI message in the chain
|
|
611
|
-
const lastAIMessage = [...messages].reverse().find(m => m.getType() === 'ai');
|
|
1276
|
+
const lastAIMessage = [...messages].reverse().find((m) => m.getType() === 'ai');
|
|
612
1277
|
|
|
613
1278
|
if (!lastAIMessage || !lastAIMessage.content) {
|
|
614
1279
|
return { content: '', messages: serializeMessages(messages) };
|
|
@@ -618,9 +1283,7 @@ async function executeAgent(
|
|
|
618
1283
|
if (typeof lastAIMessage.content === 'string') {
|
|
619
1284
|
content = lastAIMessage.content;
|
|
620
1285
|
} else if (Array.isArray(lastAIMessage.content)) {
|
|
621
|
-
content = lastAIMessage.content
|
|
622
|
-
.map((c: any) => c.text || JSON.stringify(c))
|
|
623
|
-
.join('\n');
|
|
1286
|
+
content = lastAIMessage.content.map((c: any) => c.text || JSON.stringify(c)).join('\n');
|
|
624
1287
|
} else {
|
|
625
1288
|
content = String(lastAIMessage.content);
|
|
626
1289
|
}
|
|
@@ -645,14 +1308,27 @@ function serializeMessages(messages: any[]) {
|
|
|
645
1308
|
}
|
|
646
1309
|
|
|
647
1310
|
/**
|
|
648
|
-
*
|
|
649
|
-
*
|
|
1311
|
+
* Schedule a rejection-on-timeout that also aborts the in-flight stream.
|
|
1312
|
+
* Returns the promise plus a `cancel()` so callers can release the timer
|
|
1313
|
+
* when the race resolves successfully (otherwise the handle keeps the event
|
|
1314
|
+
* loop alive until `ms` elapses).
|
|
650
1315
|
*/
|
|
651
|
-
function createTimeout(
|
|
652
|
-
|
|
653
|
-
|
|
1316
|
+
function createTimeout(
|
|
1317
|
+
ms: number,
|
|
1318
|
+
agentName: string,
|
|
1319
|
+
abortController?: AbortController,
|
|
1320
|
+
): { promise: Promise<never>; cancel: () => void } {
|
|
1321
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1322
|
+
const promise = new Promise<never>((_resolve, reject) => {
|
|
1323
|
+
timer = setTimeout(() => {
|
|
654
1324
|
abortController?.abort();
|
|
655
1325
|
reject(new Error(`Sub-agent "${agentName}" timed out after ${ms / 1000}s`));
|
|
656
|
-
}, ms)
|
|
657
|
-
);
|
|
1326
|
+
}, ms);
|
|
1327
|
+
});
|
|
1328
|
+
return {
|
|
1329
|
+
promise,
|
|
1330
|
+
cancel: () => {
|
|
1331
|
+
if (timer) clearTimeout(timer);
|
|
1332
|
+
},
|
|
1333
|
+
};
|
|
658
1334
|
}
|