plugin-agent-orchestrator 1.0.6 → 1.0.14
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.js +16 -0
- package/dist/server/collections/orchestrator-logs.js +19 -2
- package/dist/server/collections/skill-definitions.d.ts +3 -0
- package/dist/server/collections/skill-definitions.js +158 -0
- package/dist/server/collections/skill-executions.d.ts +3 -0
- package/dist/server/collections/skill-executions.js +123 -0
- package/dist/server/collections/skill-worker-configs.d.ts +3 -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-add-tracing-detail-fields.d.ts +7 -0
- package/dist/server/migrations/20260427000000-add-tracing-detail-fields.js +62 -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.d.ts +7 -0
- package/dist/server/migrations/20260429000000-add-llm-fields.js +68 -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 +160 -12
- package/dist/server/services/CodeValidator.d.ts +32 -0
- package/dist/server/services/CodeValidator.js +205 -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 +14 -0
- package/dist/server/skill-hub/tasks/SkillExecutionTask.js +267 -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 +832 -119
- 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 +368 -21
- package/src/client/TracingTab.tsx +316 -102
- package/src/client/plugin.tsx +39 -0
- 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 +17 -0
- package/src/server/collections/orchestrator-logs.ts +19 -2
- 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 +41 -0
- 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 +46 -0
- 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 +51 -3
- package/src/server/resources/tracing.ts +187 -16
- 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 +297 -0
- package/src/server/skill-hub/utils/json-fields.ts +57 -0
- package/src/server/tools/delegate-task.ts +1085 -147
- 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,678 @@ 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
|
+
|
|
29
|
+
type TraceEvent = {
|
|
30
|
+
type: string;
|
|
31
|
+
at: string;
|
|
32
|
+
title: string;
|
|
33
|
+
content?: string;
|
|
34
|
+
toolName?: string;
|
|
35
|
+
args?: any;
|
|
36
|
+
status?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type AgentExecutionResult = {
|
|
40
|
+
content: string;
|
|
41
|
+
messages: any[];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type EmployeeSkillConfig = {
|
|
45
|
+
name: string;
|
|
46
|
+
autoCall: boolean;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function sanitizeToolPart(value: string) {
|
|
50
|
+
return (value || '').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildDelegateToolName(leaderUsername: string, subAgentUsername: string) {
|
|
54
|
+
return `delegate_${sanitizeToolPart(leaderUsername)}_to_${sanitizeToolPart(subAgentUsername)}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
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;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function createDelegateToolOptions(
|
|
105
|
+
plugin: any,
|
|
106
|
+
options: {
|
|
107
|
+
leaderUsername: string;
|
|
108
|
+
subAgentUsername: string;
|
|
109
|
+
subAgentEmployee: any;
|
|
110
|
+
maxDepth?: number;
|
|
111
|
+
timeout?: number;
|
|
112
|
+
toolName: string;
|
|
113
|
+
legacyAlias?: boolean;
|
|
114
|
+
llmService?: string;
|
|
115
|
+
model?: string;
|
|
116
|
+
recursionLimit?: number;
|
|
117
|
+
},
|
|
118
|
+
) {
|
|
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);
|
|
132
|
+
const toolDescription = [
|
|
133
|
+
`Delegate a task from "${leaderUsername}" to the AI Employee "${subAgentEmployee.nickname || subAgentUsername}".`,
|
|
134
|
+
legacyAlias ? 'This is a backward-compatible alias for existing skill assignments.' : '',
|
|
135
|
+
subAgentEmployee.about ? `Specialist profile: ${subAgentEmployee.about.substring(0, 200)}` : '',
|
|
136
|
+
'The sub-agent will execute the task independently and return its final answer.',
|
|
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(' ');
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
scope: 'CUSTOM',
|
|
144
|
+
execution: 'backend',
|
|
145
|
+
defaultPermission: 'ALLOW',
|
|
146
|
+
silence: false,
|
|
147
|
+
introduction: {
|
|
148
|
+
title: `[${leaderUsername}] ${subAgentEmployee.nickname || subAgentUsername}${legacyAlias ? ' (legacy)' : ''}`,
|
|
149
|
+
about: toolDescription,
|
|
150
|
+
},
|
|
151
|
+
definition: {
|
|
152
|
+
name: toolName,
|
|
153
|
+
description: toolDescription,
|
|
154
|
+
schema: z.object({
|
|
155
|
+
task: z.string().describe('The detailed task description for the sub-agent to execute.'),
|
|
156
|
+
context: z
|
|
157
|
+
.string()
|
|
158
|
+
.optional()
|
|
159
|
+
.describe('Optional additional context to help the sub-agent understand the task better.'),
|
|
160
|
+
}),
|
|
161
|
+
},
|
|
162
|
+
invoke: async (ctx: Context, args: { task: string; context?: string }, id: string) => {
|
|
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
|
+
}
|
|
182
|
+
if (callingEmployee && callingEmployee !== leaderUsername) {
|
|
183
|
+
await logDelegation(ctx, plugin, {
|
|
184
|
+
leaderUsername,
|
|
185
|
+
subAgentUsername,
|
|
186
|
+
toolName,
|
|
187
|
+
task: args.task,
|
|
188
|
+
context: args.context,
|
|
189
|
+
result: '',
|
|
190
|
+
status: 'error',
|
|
191
|
+
depth: (ctx as any)[ORCHESTRATOR_DEPTH_KEY] ?? 0,
|
|
192
|
+
durationMs: 0,
|
|
193
|
+
error: `Employee "${callingEmployee}" is not authorized to use this delegation rule.`,
|
|
194
|
+
});
|
|
195
|
+
return {
|
|
196
|
+
status: 'error' as const,
|
|
197
|
+
content: `Employee "${callingEmployee}" is not authorized to delegate to "${subAgentUsername}". Configure an orchestration rule first.`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return invokeDelegateTask(ctx, plugin, {
|
|
202
|
+
leaderUsername,
|
|
203
|
+
subAgentUsername,
|
|
204
|
+
subAgentEmployee,
|
|
205
|
+
task: args.task,
|
|
206
|
+
context: args.context,
|
|
207
|
+
maxDepth: maxDepth ?? 1,
|
|
208
|
+
timeout: timeout ?? 120000,
|
|
209
|
+
toolCallId: id,
|
|
210
|
+
toolName,
|
|
211
|
+
llmService,
|
|
212
|
+
model,
|
|
213
|
+
recursionLimit,
|
|
214
|
+
});
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
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 || {};
|
|
465
|
+
const raw =
|
|
466
|
+
(ctx as any)._currentAIEmployee ||
|
|
467
|
+
(ctx as any).state?.currentAIEmployee ||
|
|
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
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function truncateText(value: any, maxLen: number) {
|
|
490
|
+
const text = typeof value === 'string' ? value : value == null ? '' : JSON.stringify(value);
|
|
491
|
+
return text.length > maxLen ? `${text.slice(0, maxLen)}\n...[truncated]` : text;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function nowIso() {
|
|
495
|
+
return new Date().toISOString();
|
|
496
|
+
}
|
|
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
|
+
|
|
16
694
|
/**
|
|
17
695
|
* Creates one dynamic tool per configured sub-agent for a given leader.
|
|
18
696
|
* Uses Strategy B (Per-SubAgent Tool): each sub-agent becomes a separate tool
|
|
@@ -28,96 +706,19 @@ const ORCHESTRATOR_DEPTH_KEY = '__orchestratorDepth';
|
|
|
28
706
|
* leaderUsername field, so scoping is enforced in the invoke callback)
|
|
29
707
|
*/
|
|
30
708
|
export function createDelegateToolsProvider(plugin: any) {
|
|
709
|
+
attachInvalidationHooks(plugin);
|
|
710
|
+
|
|
31
711
|
return async (register: any) => {
|
|
32
712
|
try {
|
|
33
|
-
|
|
34
|
-
if (!
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
});
|
|
39
|
-
if (!configs?.length) return;
|
|
40
|
-
|
|
41
|
-
// Build a lookup: which leaders are allowed to use each delegate tool
|
|
42
|
-
// Multiple leaders may share the same sub-agent, but each leader must
|
|
43
|
-
// have an explicit rule.
|
|
44
|
-
const leadersByTool = new Map<string, Set<string>>();
|
|
45
|
-
for (const config of configs) {
|
|
46
|
-
const toolName = `delegate_to_${config.subAgentUsername.replace(/[^a-zA-Z0-9_-]/g, '_')}`;
|
|
47
|
-
if (!leadersByTool.has(toolName)) {
|
|
48
|
-
leadersByTool.set(toolName, new Set());
|
|
49
|
-
}
|
|
50
|
-
leadersByTool.get(toolName)!.add(config.leaderUsername);
|
|
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);
|
|
51
718
|
}
|
|
52
719
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
for (const config of configs) {
|
|
57
|
-
const { leaderUsername, subAgentUsername, maxDepth, timeout } = config;
|
|
58
|
-
|
|
59
|
-
// Skip if we already registered this sub-agent's tool
|
|
60
|
-
if (seenSubAgents.has(subAgentUsername)) continue;
|
|
61
|
-
seenSubAgents.add(subAgentUsername);
|
|
62
|
-
|
|
63
|
-
// Fetch the sub-agent employee model for its description and LLM config
|
|
64
|
-
const subAgentEmployee = await plugin.db.getRepository('aiEmployees').findOne({
|
|
65
|
-
filter: { username: subAgentUsername },
|
|
66
|
-
});
|
|
67
|
-
if (!subAgentEmployee) continue;
|
|
68
|
-
|
|
69
|
-
const toolName = `delegate_to_${subAgentUsername.replace(/[^a-zA-Z0-9_-]/g, '_')}`;
|
|
70
|
-
const toolDescription = [
|
|
71
|
-
`Delegate a task to the AI Employee "${subAgentEmployee.nickname || subAgentUsername}".`,
|
|
72
|
-
subAgentEmployee.about ? `Specialist profile: ${subAgentEmployee.about.substring(0, 200)}` : '',
|
|
73
|
-
'The sub-agent will execute the task independently and return its final answer.',
|
|
74
|
-
].filter(Boolean).join(' ');
|
|
75
|
-
|
|
76
|
-
// Capture the allowed leaders for this tool (for invoke-time scoping)
|
|
77
|
-
const allowedLeaders = leadersByTool.get(toolName)!;
|
|
78
|
-
|
|
79
|
-
register.registerTools({
|
|
80
|
-
scope: 'CUSTOM',
|
|
81
|
-
execution: 'backend',
|
|
82
|
-
defaultPermission: 'ALLOW',
|
|
83
|
-
silence: false,
|
|
84
|
-
introduction: {
|
|
85
|
-
title: `[Sub-Agent] ${subAgentEmployee.nickname || subAgentUsername}`,
|
|
86
|
-
about: toolDescription,
|
|
87
|
-
},
|
|
88
|
-
definition: {
|
|
89
|
-
name: toolName,
|
|
90
|
-
description: toolDescription,
|
|
91
|
-
schema: z.object({
|
|
92
|
-
task: z.string().describe('The detailed task description for the sub-agent to execute.'),
|
|
93
|
-
context: z.string().optional().describe('Optional additional context to help the sub-agent understand the task better.'),
|
|
94
|
-
}),
|
|
95
|
-
},
|
|
96
|
-
invoke: async (ctx: Context, args: { task: string; context?: string }, id: string) => {
|
|
97
|
-
// --- P2 FIX: Per-leader scoping at invoke time ---
|
|
98
|
-
// Core ToolsOptions doesn't support leaderUsername field, so
|
|
99
|
-
// we enforce scoping here by checking the calling employee.
|
|
100
|
-
const callingEmployee = (ctx as any)._currentAIEmployee?.username
|
|
101
|
-
|| (ctx as any).state?.currentAIEmployee;
|
|
102
|
-
if (callingEmployee && !allowedLeaders.has(callingEmployee)) {
|
|
103
|
-
return {
|
|
104
|
-
status: 'error' as const,
|
|
105
|
-
content: `Employee "${callingEmployee}" is not authorized to delegate to "${subAgentUsername}". Configure an orchestration rule first.`,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return invokeDelegateTask(ctx, plugin, {
|
|
110
|
-
leaderUsername: callingEmployee || Array.from(allowedLeaders)[0] || '',
|
|
111
|
-
subAgentUsername,
|
|
112
|
-
subAgentEmployee,
|
|
113
|
-
task: args.task,
|
|
114
|
-
context: args.context,
|
|
115
|
-
maxDepth: maxDepth ?? 1,
|
|
116
|
-
timeout: timeout ?? 120000,
|
|
117
|
-
toolCallId: id,
|
|
118
|
-
});
|
|
119
|
-
},
|
|
120
|
-
});
|
|
720
|
+
if (toolsCache.tools.length) {
|
|
721
|
+
register.registerTools(toolsCache.tools);
|
|
121
722
|
}
|
|
122
723
|
} catch (e) {
|
|
123
724
|
plugin.app.log.error('[AgentOrchestrator] Failed to register delegate tools', e);
|
|
@@ -125,6 +726,14 @@ export function createDelegateToolsProvider(plugin: any) {
|
|
|
125
726
|
};
|
|
126
727
|
}
|
|
127
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
|
+
|
|
128
737
|
/**
|
|
129
738
|
* Core execution logic using createReactAgent (public LangGraph API).
|
|
130
739
|
*
|
|
@@ -153,20 +762,109 @@ async function invokeDelegateTask(
|
|
|
153
762
|
maxDepth: number;
|
|
154
763
|
timeout: number;
|
|
155
764
|
toolCallId: string;
|
|
765
|
+
toolName: string;
|
|
766
|
+
llmService?: string;
|
|
767
|
+
model?: string;
|
|
768
|
+
recursionLimit?: number;
|
|
769
|
+
rootRunId?: string;
|
|
770
|
+
parentSpanId?: string;
|
|
156
771
|
},
|
|
157
772
|
) {
|
|
158
|
-
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);
|
|
159
796
|
|
|
160
797
|
// --- P1: Depth enforcement ---
|
|
161
798
|
const currentDepth: number = (ctx as any)[ORCHESTRATOR_DEPTH_KEY] ?? 0;
|
|
162
799
|
if (currentDepth >= maxDepth) {
|
|
800
|
+
await logDelegation(ctx, plugin, {
|
|
801
|
+
leaderUsername,
|
|
802
|
+
subAgentUsername,
|
|
803
|
+
toolName,
|
|
804
|
+
task,
|
|
805
|
+
context,
|
|
806
|
+
result: '',
|
|
807
|
+
status: 'error',
|
|
808
|
+
depth: currentDepth,
|
|
809
|
+
durationMs: 0,
|
|
810
|
+
error: `Delegation depth limit reached (${currentDepth}/${maxDepth}).`,
|
|
811
|
+
snapshot: ctxSnapshot,
|
|
812
|
+
});
|
|
163
813
|
return {
|
|
164
814
|
status: 'error' as const,
|
|
165
815
|
content: `Delegation depth limit reached (${currentDepth}/${maxDepth}). Sub-agent "${subAgentUsername}" cannot delegate further.`,
|
|
166
816
|
};
|
|
167
817
|
}
|
|
168
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;
|
|
169
824
|
const startTime = Date.now();
|
|
825
|
+
const trace: TraceEvent[] = [
|
|
826
|
+
{
|
|
827
|
+
type: 'start',
|
|
828
|
+
at: nowIso(),
|
|
829
|
+
title: `Delegation started: ${leaderUsername} -> ${subAgentUsername}`,
|
|
830
|
+
content: task,
|
|
831
|
+
},
|
|
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;
|
|
852
|
+
const logRecord = await logDelegation(ctx, plugin, {
|
|
853
|
+
leaderUsername,
|
|
854
|
+
subAgentUsername,
|
|
855
|
+
toolName,
|
|
856
|
+
task,
|
|
857
|
+
context,
|
|
858
|
+
result: '',
|
|
859
|
+
status: 'running',
|
|
860
|
+
depth: currentDepth,
|
|
861
|
+
durationMs: 0,
|
|
862
|
+
trace,
|
|
863
|
+
snapshot: ctxSnapshot,
|
|
864
|
+
});
|
|
865
|
+
if (executionSpanId && logRecord?.id) {
|
|
866
|
+
await spanService.update(executionSpanId, { orchestratorLogId: logRecord.id });
|
|
867
|
+
}
|
|
170
868
|
|
|
171
869
|
try {
|
|
172
870
|
const aiPlugin = ctx.app.pm.get('ai') as PluginAIServer;
|
|
@@ -175,9 +873,33 @@ async function invokeDelegateTask(
|
|
|
175
873
|
}
|
|
176
874
|
|
|
177
875
|
// --- Step 1: Resolve LLM model from sub-agent's employee config ---
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
876
|
+
let modelSettings = hasModelSettings(subAgentEmployee.modelSettings) ? subAgentEmployee.modelSettings : undefined;
|
|
877
|
+
|
|
878
|
+
// Override with orchestrator config if provided
|
|
879
|
+
if (llmService && model) {
|
|
880
|
+
modelSettings = { llmService, model };
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (!hasModelSettings(modelSettings)) {
|
|
884
|
+
// Fallback to leader's LLM model if sub-agent doesn't have one
|
|
885
|
+
const leaderEmployee = await plugin.db.getRepository('aiEmployees').findOne({
|
|
886
|
+
filter: { username: leaderUsername },
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
// The leader's model might be empty in the DB if it relies on the dynamic system default.
|
|
890
|
+
// In that case, we extract the dynamic `model` passed from the frontend request.
|
|
891
|
+
const dynamicModel = ctx.action?.params?.values?.model;
|
|
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
|
+
);
|
|
902
|
+
}
|
|
181
903
|
}
|
|
182
904
|
|
|
183
905
|
const { provider } = await aiPlugin.aiManager.getLLMService({
|
|
@@ -194,38 +916,123 @@ async function invokeDelegateTask(
|
|
|
194
916
|
|
|
195
917
|
// skillSettings.skills is { name: string, autoCall: boolean }[]
|
|
196
918
|
// (verified at ai-employee.ts:1028-1029)
|
|
197
|
-
const employeeSkills:
|
|
198
|
-
.map((s: any) =>
|
|
199
|
-
|
|
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]));
|
|
200
927
|
|
|
201
928
|
const langchainTools: DynamicStructuredTool[] = [];
|
|
202
929
|
|
|
203
930
|
for (const toolEntry of allTools) {
|
|
204
|
-
const
|
|
205
|
-
if (!
|
|
931
|
+
const entryName = toolEntry.definition.name;
|
|
932
|
+
if (!entryName) continue;
|
|
933
|
+
const employeeSkill = employeeSkillMap.get(entryName);
|
|
206
934
|
|
|
207
935
|
// Only include tools that the sub-agent employee is configured to use.
|
|
208
|
-
// Also skip our own
|
|
936
|
+
// Also skip our own orchestration tools to prevent circular delegation
|
|
209
937
|
// (belt-and-suspenders with the depth check above).
|
|
210
|
-
|
|
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
|
+
) {
|
|
211
949
|
continue;
|
|
212
950
|
}
|
|
213
951
|
|
|
214
952
|
langchainTools.push(
|
|
215
953
|
new DynamicStructuredTool({
|
|
216
|
-
name:
|
|
217
|
-
description: toolEntry.definition.description ||
|
|
954
|
+
name: entryName.replace(/[^a-zA-Z0-9_-]/g, '_'),
|
|
955
|
+
description: toolEntry.definition.description || entryName,
|
|
218
956
|
schema: (toolEntry.definition.schema || z.object({})) as any,
|
|
219
957
|
func: async (toolArgs) => {
|
|
220
958
|
// Forward the invoke with depth tracking
|
|
221
959
|
const invokeCtx = Object.create(ctx);
|
|
222
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
|
+
});
|
|
990
|
+
|
|
991
|
+
trace.push({
|
|
992
|
+
type: 'tool_call',
|
|
993
|
+
at: nowIso(),
|
|
994
|
+
title: `Calling tool: ${toolEntry.definition.name}`,
|
|
995
|
+
toolName: toolEntry.definition.name,
|
|
996
|
+
args: toolArgs,
|
|
997
|
+
});
|
|
223
998
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
999
|
+
try {
|
|
1000
|
+
const res = await toolEntry.invoke(invokeCtx, toolArgs, `orch-${toolCallId}`);
|
|
1001
|
+
const output = truncateText(res?.content ?? res?.result ?? res, 50000);
|
|
1002
|
+
trace.push({
|
|
1003
|
+
type: 'tool_result',
|
|
1004
|
+
at: nowIso(),
|
|
1005
|
+
title: `Tool finished: ${toolEntry.definition.name}`,
|
|
1006
|
+
toolName: toolEntry.definition.name,
|
|
1007
|
+
status: res?.status || 'success',
|
|
1008
|
+
content: truncateText(output, 2000),
|
|
1009
|
+
});
|
|
1010
|
+
if (res?.status === 'error') {
|
|
1011
|
+
await spanService.finish(toolSpanId, 'error', toolStartedAt, {
|
|
1012
|
+
output,
|
|
1013
|
+
error: truncateText(res.content || output, 10000),
|
|
1014
|
+
});
|
|
1015
|
+
throw new Error(`Tool <${toolEntry.definition.name}> failed: ${res.content}`);
|
|
1016
|
+
}
|
|
1017
|
+
await spanService.finish(toolSpanId, 'success', toolStartedAt, {
|
|
1018
|
+
output,
|
|
1019
|
+
skillExecutionId: res?.result?.execId || res?.execId,
|
|
1020
|
+
});
|
|
1021
|
+
return typeof res?.content === 'string' ? res.content : JSON.stringify(res);
|
|
1022
|
+
} catch (e: any) {
|
|
1023
|
+
trace.push({
|
|
1024
|
+
type: 'tool_error',
|
|
1025
|
+
at: nowIso(),
|
|
1026
|
+
title: `Tool failed: ${toolEntry.definition.name}`,
|
|
1027
|
+
toolName: toolEntry.definition.name,
|
|
1028
|
+
status: 'error',
|
|
1029
|
+
content: e.message,
|
|
1030
|
+
});
|
|
1031
|
+
await spanService.finish(toolSpanId, 'error', toolStartedAt, {
|
|
1032
|
+
error: truncateText(e.message, 10000),
|
|
1033
|
+
});
|
|
1034
|
+
throw e;
|
|
227
1035
|
}
|
|
228
|
-
return typeof res?.content === 'string' ? res.content : JSON.stringify(res);
|
|
229
1036
|
},
|
|
230
1037
|
}),
|
|
231
1038
|
);
|
|
@@ -239,35 +1046,72 @@ async function invokeDelegateTask(
|
|
|
239
1046
|
});
|
|
240
1047
|
|
|
241
1048
|
// --- Step 4: Construct messages ---
|
|
242
|
-
const systemPrompt =
|
|
243
|
-
|
|
244
|
-
|
|
1049
|
+
const systemPrompt =
|
|
1050
|
+
subAgentEmployee.chatSettings?.systemPrompt ||
|
|
1051
|
+
subAgentEmployee.bio ||
|
|
1052
|
+
`You are an AI assistant named "${subAgentEmployee.nickname || subAgentUsername}". ${
|
|
1053
|
+
subAgentEmployee.about || ''
|
|
1054
|
+
}`;
|
|
245
1055
|
|
|
246
|
-
const combinedTask = context
|
|
247
|
-
? `Task: ${task}\n\nContext Provided:\n${context}`
|
|
248
|
-
: `Task: ${task}`;
|
|
1056
|
+
const combinedTask = context ? `Task: ${task}\n\nContext Provided:\n${context}` : `Task: ${task}`;
|
|
249
1057
|
|
|
250
1058
|
// --- Step 5: Execute with timeout + abort ---
|
|
251
1059
|
// P3 FIX: AbortController signal cancels the in-flight stream on timeout,
|
|
252
1060
|
// preventing continued token consumption after the timeout fires.
|
|
253
|
-
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
|
+
);
|
|
254
1070
|
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
+
}
|
|
259
1079
|
|
|
260
|
-
const content =
|
|
1080
|
+
const content = result.content || 'Sub-agent completed the task but produced no output.';
|
|
1081
|
+
trace.push({
|
|
1082
|
+
type: 'finish',
|
|
1083
|
+
at: nowIso(),
|
|
1084
|
+
title: `Delegation finished: ${subAgentUsername}`,
|
|
1085
|
+
status: 'success',
|
|
1086
|
+
content: truncateText(content, 2000),
|
|
1087
|
+
});
|
|
261
1088
|
|
|
262
1089
|
// Log successful execution for tracing
|
|
263
1090
|
await logDelegation(ctx, plugin, {
|
|
1091
|
+
id: logRecord?.id,
|
|
264
1092
|
leaderUsername,
|
|
265
1093
|
subAgentUsername,
|
|
1094
|
+
toolName,
|
|
266
1095
|
task,
|
|
1096
|
+
context,
|
|
267
1097
|
result: content,
|
|
268
1098
|
status: 'success',
|
|
269
1099
|
depth: currentDepth,
|
|
270
1100
|
durationMs: Date.now() - startTime,
|
|
1101
|
+
trace,
|
|
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
|
+
},
|
|
271
1115
|
});
|
|
272
1116
|
|
|
273
1117
|
return {
|
|
@@ -279,15 +1123,41 @@ async function invokeDelegateTask(
|
|
|
279
1123
|
|
|
280
1124
|
// Log failed execution for tracing
|
|
281
1125
|
await logDelegation(ctx, plugin, {
|
|
1126
|
+
id: logRecord?.id,
|
|
282
1127
|
leaderUsername,
|
|
283
1128
|
subAgentUsername,
|
|
1129
|
+
toolName,
|
|
284
1130
|
task,
|
|
1131
|
+
context,
|
|
285
1132
|
result: '',
|
|
286
1133
|
status: 'error',
|
|
287
1134
|
depth: currentDepth,
|
|
288
1135
|
durationMs: Date.now() - startTime,
|
|
289
1136
|
error: e.message,
|
|
290
|
-
|
|
1137
|
+
trace: [
|
|
1138
|
+
...trace,
|
|
1139
|
+
{
|
|
1140
|
+
type: 'error',
|
|
1141
|
+
at: nowIso(),
|
|
1142
|
+
title: `Delegation failed: ${subAgentUsername}`,
|
|
1143
|
+
status: 'error',
|
|
1144
|
+
content: e.message,
|
|
1145
|
+
},
|
|
1146
|
+
],
|
|
1147
|
+
snapshot: ctxSnapshot,
|
|
1148
|
+
}).catch((logErr) => {
|
|
1149
|
+
plugin.app.log.warn('[AgentOrchestrator] Failed to save error log for delegation', logErr);
|
|
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
|
+
});
|
|
291
1161
|
|
|
292
1162
|
return {
|
|
293
1163
|
status: 'error' as const,
|
|
@@ -303,34 +1173,73 @@ async function logDelegation(
|
|
|
303
1173
|
ctx: Context,
|
|
304
1174
|
plugin: any,
|
|
305
1175
|
data: {
|
|
1176
|
+
id?: number | string;
|
|
306
1177
|
leaderUsername: string;
|
|
307
1178
|
subAgentUsername: string;
|
|
1179
|
+
toolName: string;
|
|
308
1180
|
task: string;
|
|
1181
|
+
context?: string;
|
|
309
1182
|
result: string;
|
|
310
1183
|
status: string;
|
|
311
1184
|
depth: number;
|
|
312
1185
|
durationMs: number;
|
|
313
1186
|
error?: string;
|
|
1187
|
+
trace?: TraceEvent[];
|
|
1188
|
+
messages?: any[];
|
|
1189
|
+
snapshot?: CtxSnapshot;
|
|
314
1190
|
},
|
|
315
1191
|
) {
|
|
316
1192
|
try {
|
|
317
1193
|
const logsRepo = plugin.db.getRepository('orchestratorLogs');
|
|
318
|
-
if (!logsRepo)
|
|
1194
|
+
if (!logsRepo) {
|
|
1195
|
+
plugin.app.log.warn('[AgentOrchestrator] orchestratorLogs repository not found — skipping log');
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
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
|
+
}
|
|
1209
|
+
}
|
|
319
1210
|
|
|
320
|
-
|
|
1211
|
+
const values = {
|
|
1212
|
+
leaderUsername: data.leaderUsername,
|
|
1213
|
+
subAgentUsername: data.subAgentUsername,
|
|
1214
|
+
toolName: data.toolName,
|
|
1215
|
+
task: truncateText(data.task, 10000),
|
|
1216
|
+
context: truncateText(data.context || '', 10000),
|
|
1217
|
+
result: truncateText(data.result || '', 50000),
|
|
1218
|
+
status: data.status,
|
|
1219
|
+
depth: data.depth,
|
|
1220
|
+
durationMs: data.durationMs,
|
|
1221
|
+
error: truncateText(data.error || '', 10000),
|
|
1222
|
+
trace: data.trace || [],
|
|
1223
|
+
messages: data.messages || [],
|
|
1224
|
+
userId,
|
|
1225
|
+
updatedAt: new Date(),
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
if (data.id) {
|
|
1229
|
+
await logsRepo.update({
|
|
1230
|
+
filterByTk: data.id,
|
|
1231
|
+
values,
|
|
1232
|
+
});
|
|
1233
|
+
return { id: data.id };
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
const record = await logsRepo.create({
|
|
321
1237
|
values: {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
toolName: `delegate_to_${data.subAgentUsername}`,
|
|
325
|
-
task: (data.task || '').substring(0, 2000),
|
|
326
|
-
result: (data.result || '').substring(0, 5000),
|
|
327
|
-
status: data.status,
|
|
328
|
-
depth: data.depth,
|
|
329
|
-
durationMs: data.durationMs,
|
|
330
|
-
error: (data.error || '').substring(0, 2000),
|
|
331
|
-
userId: ctx.auth?.user?.id || ctx.state?.currentUser?.id,
|
|
1238
|
+
...values,
|
|
1239
|
+
createdAt: new Date(),
|
|
332
1240
|
},
|
|
333
1241
|
});
|
|
1242
|
+
return record?.toJSON?.() || record;
|
|
334
1243
|
} catch (e) {
|
|
335
1244
|
plugin.app.log.warn('[AgentOrchestrator] Failed to log delegation event', e);
|
|
336
1245
|
}
|
|
@@ -346,8 +1255,9 @@ async function executeAgent(
|
|
|
346
1255
|
systemPrompt: string,
|
|
347
1256
|
task: string,
|
|
348
1257
|
signal?: AbortSignal,
|
|
349
|
-
|
|
350
|
-
|
|
1258
|
+
recursionLimit = 50,
|
|
1259
|
+
): Promise<AgentExecutionResult> {
|
|
1260
|
+
const config: any = { recursionLimit };
|
|
351
1261
|
if (signal) {
|
|
352
1262
|
config.signal = signal;
|
|
353
1263
|
}
|
|
@@ -361,36 +1271,64 @@ async function executeAgent(
|
|
|
361
1271
|
|
|
362
1272
|
// finalState.messages contains the entire conversation history of this delegation
|
|
363
1273
|
const messages = finalState?.messages || [];
|
|
364
|
-
|
|
1274
|
+
|
|
365
1275
|
// Find the last AI message in the chain
|
|
366
|
-
const lastAIMessage = [...messages].reverse().find(m => m.getType() === 'ai');
|
|
1276
|
+
const lastAIMessage = [...messages].reverse().find((m) => m.getType() === 'ai');
|
|
367
1277
|
|
|
368
1278
|
if (!lastAIMessage || !lastAIMessage.content) {
|
|
369
|
-
return '';
|
|
1279
|
+
return { content: '', messages: serializeMessages(messages) };
|
|
370
1280
|
}
|
|
371
1281
|
|
|
1282
|
+
let content = '';
|
|
372
1283
|
if (typeof lastAIMessage.content === 'string') {
|
|
373
|
-
|
|
1284
|
+
content = lastAIMessage.content;
|
|
1285
|
+
} else if (Array.isArray(lastAIMessage.content)) {
|
|
1286
|
+
content = lastAIMessage.content.map((c: any) => c.text || JSON.stringify(c)).join('\n');
|
|
1287
|
+
} else {
|
|
1288
|
+
content = String(lastAIMessage.content);
|
|
374
1289
|
}
|
|
375
1290
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
.map((c: any) => c.text || JSON.stringify(c))
|
|
379
|
-
.join('\n');
|
|
380
|
-
}
|
|
1291
|
+
return { content, messages: serializeMessages(messages) };
|
|
1292
|
+
}
|
|
381
1293
|
|
|
382
|
-
|
|
1294
|
+
function serializeMessages(messages: any[]) {
|
|
1295
|
+
return (messages || []).map((message, index) => {
|
|
1296
|
+
const type = typeof message.getType === 'function' ? message.getType() : message.type;
|
|
1297
|
+
return {
|
|
1298
|
+
index,
|
|
1299
|
+
type,
|
|
1300
|
+
name: message.name,
|
|
1301
|
+
content: truncateText(message.content, 10000),
|
|
1302
|
+
toolCalls: message.tool_calls || message.toolCalls || [],
|
|
1303
|
+
toolCallId: message.tool_call_id,
|
|
1304
|
+
additionalKwargs: message.additional_kwargs,
|
|
1305
|
+
responseMetadata: message.response_metadata,
|
|
1306
|
+
};
|
|
1307
|
+
});
|
|
383
1308
|
}
|
|
384
1309
|
|
|
385
1310
|
/**
|
|
386
|
-
*
|
|
387
|
-
*
|
|
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).
|
|
388
1315
|
*/
|
|
389
|
-
function createTimeout(
|
|
390
|
-
|
|
391
|
-
|
|
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(() => {
|
|
392
1324
|
abortController?.abort();
|
|
393
1325
|
reject(new Error(`Sub-agent "${agentName}" timed out after ${ms / 1000}s`));
|
|
394
|
-
}, ms)
|
|
395
|
-
);
|
|
1326
|
+
}, ms);
|
|
1327
|
+
});
|
|
1328
|
+
return {
|
|
1329
|
+
promise,
|
|
1330
|
+
cancel: () => {
|
|
1331
|
+
if (timer) clearTimeout(timer);
|
|
1332
|
+
},
|
|
1333
|
+
};
|
|
396
1334
|
}
|