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.
Files changed (257) hide show
  1. package/README.md +16 -291
  2. package/dist/client/AIEmployeesContext.d.ts +7 -0
  3. package/dist/client/OrchestratorSettings.d.ts +2 -1
  4. package/dist/client/index.js +1 -1
  5. package/dist/client/plugin.d.ts +1 -0
  6. package/dist/client/skill-hub/components/ExecutionHistory.d.ts +2 -0
  7. package/dist/client/skill-hub/components/ExecutionProgress.d.ts +20 -0
  8. package/dist/client/skill-hub/components/GitSkillImport.d.ts +7 -0
  9. package/dist/client/skill-hub/components/SkillEditor.d.ts +7 -0
  10. package/dist/client/skill-hub/components/SkillManager.d.ts +2 -0
  11. package/dist/client/skill-hub/components/SkillMetrics.d.ts +2 -0
  12. package/dist/client/skill-hub/components/SkillTestPanel.d.ts +7 -0
  13. package/dist/client/skill-hub/index.d.ts +10 -0
  14. package/dist/client/skill-hub/locale.d.ts +3 -0
  15. package/dist/client/skill-hub/tools/InteractionSchemasProvider.d.ts +19 -0
  16. package/dist/client/skill-hub/tools/SkillHubCard.d.ts +3 -0
  17. package/dist/client/skill-hub/utils/jsonFields.d.ts +3 -0
  18. package/dist/externalVersion.js +6 -6
  19. package/dist/node_modules/adm-zip/LICENSE +21 -0
  20. package/dist/node_modules/adm-zip/adm-zip.js +1 -0
  21. package/dist/node_modules/adm-zip/headers/entryHeader.js +377 -0
  22. package/dist/node_modules/adm-zip/headers/index.js +2 -0
  23. package/dist/node_modules/adm-zip/headers/mainHeader.js +130 -0
  24. package/dist/node_modules/adm-zip/methods/deflater.js +33 -0
  25. package/dist/node_modules/adm-zip/methods/index.js +3 -0
  26. package/dist/node_modules/adm-zip/methods/inflater.js +34 -0
  27. package/dist/node_modules/adm-zip/methods/zipcrypto.js +175 -0
  28. package/dist/node_modules/adm-zip/package.json +1 -0
  29. package/dist/node_modules/adm-zip/util/constants.js +142 -0
  30. package/dist/node_modules/adm-zip/util/decoder.js +5 -0
  31. package/dist/node_modules/adm-zip/util/errors.js +63 -0
  32. package/dist/node_modules/adm-zip/util/fattr.js +76 -0
  33. package/dist/node_modules/adm-zip/util/index.js +5 -0
  34. package/dist/node_modules/adm-zip/util/utils.js +339 -0
  35. package/dist/node_modules/adm-zip/zipEntry.js +405 -0
  36. package/dist/node_modules/adm-zip/zipFile.js +446 -0
  37. package/dist/node_modules/simple-git/dist/cjs/index.js +7399 -0
  38. package/dist/node_modules/simple-git/dist/esm/index.js +4745 -0
  39. package/dist/node_modules/simple-git/dist/esm/package.json +3 -0
  40. package/dist/node_modules/simple-git/dist/src/lib/api.d.ts +13 -0
  41. package/dist/node_modules/simple-git/dist/src/lib/args/log-format.d.ts +9 -0
  42. package/dist/node_modules/simple-git/dist/src/lib/errors/git-construct-error.d.ts +15 -0
  43. package/dist/node_modules/simple-git/dist/src/lib/errors/git-error.d.ts +30 -0
  44. package/dist/node_modules/simple-git/dist/src/lib/errors/git-plugin-error.d.ts +7 -0
  45. package/dist/node_modules/simple-git/dist/src/lib/errors/git-response-error.d.ts +32 -0
  46. package/dist/node_modules/simple-git/dist/src/lib/errors/task-configuration-error.d.ts +12 -0
  47. package/dist/node_modules/simple-git/dist/src/lib/git-factory.d.ts +15 -0
  48. package/dist/node_modules/simple-git/dist/src/lib/git-logger.d.ts +21 -0
  49. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-branch-delete.d.ts +5 -0
  50. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-branch.d.ts +2 -0
  51. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-commit.d.ts +2 -0
  52. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-diff-summary.d.ts +3 -0
  53. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-fetch.d.ts +2 -0
  54. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-list-log-summary.d.ts +6 -0
  55. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-merge.d.ts +11 -0
  56. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-move.d.ts +2 -0
  57. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-pull.d.ts +6 -0
  58. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-push.d.ts +4 -0
  59. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-remote-messages.d.ts +5 -0
  60. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-remote-objects.d.ts +3 -0
  61. package/dist/node_modules/simple-git/dist/src/lib/plugins/abort-plugin.d.ts +3 -0
  62. package/dist/node_modules/simple-git/dist/src/lib/plugins/block-unsafe-operations-plugin.d.ts +3 -0
  63. package/dist/node_modules/simple-git/dist/src/lib/plugins/command-config-prefixing-plugin.d.ts +2 -0
  64. package/dist/node_modules/simple-git/dist/src/lib/plugins/completion-detection.plugin.d.ts +3 -0
  65. package/dist/node_modules/simple-git/dist/src/lib/plugins/custom-binary.plugin.d.ts +3 -0
  66. package/dist/node_modules/simple-git/dist/src/lib/plugins/error-detection.plugin.d.ts +7 -0
  67. package/dist/node_modules/simple-git/dist/src/lib/plugins/index.d.ts +11 -0
  68. package/dist/node_modules/simple-git/dist/src/lib/plugins/plugin-store.d.ts +11 -0
  69. package/dist/node_modules/simple-git/dist/src/lib/plugins/progress-monitor-plugin.d.ts +3 -0
  70. package/dist/node_modules/simple-git/dist/src/lib/plugins/simple-git-plugin.d.ts +48 -0
  71. package/dist/node_modules/simple-git/dist/src/lib/plugins/spawn-options-plugin.d.ts +3 -0
  72. package/dist/node_modules/simple-git/dist/src/lib/plugins/suffix-paths.plugin.d.ts +2 -0
  73. package/dist/node_modules/simple-git/dist/src/lib/plugins/timout-plugin.d.ts +3 -0
  74. package/dist/node_modules/simple-git/dist/src/lib/responses/BranchDeleteSummary.d.ts +12 -0
  75. package/dist/node_modules/simple-git/dist/src/lib/responses/BranchSummary.d.ts +14 -0
  76. package/dist/node_modules/simple-git/dist/src/lib/responses/CheckIgnore.d.ts +4 -0
  77. package/dist/node_modules/simple-git/dist/src/lib/responses/CleanSummary.d.ts +9 -0
  78. package/dist/node_modules/simple-git/dist/src/lib/responses/ConfigList.d.ts +13 -0
  79. package/dist/node_modules/simple-git/dist/src/lib/responses/DiffSummary.d.ts +10 -0
  80. package/dist/node_modules/simple-git/dist/src/lib/responses/FileStatusSummary.d.ts +9 -0
  81. package/dist/node_modules/simple-git/dist/src/lib/responses/GetRemoteSummary.d.ts +11 -0
  82. package/dist/node_modules/simple-git/dist/src/lib/responses/InitSummary.d.ts +9 -0
  83. package/dist/node_modules/simple-git/dist/src/lib/responses/MergeSummary.d.ts +16 -0
  84. package/dist/node_modules/simple-git/dist/src/lib/responses/PullSummary.d.ts +25 -0
  85. package/dist/node_modules/simple-git/dist/src/lib/responses/StatusSummary.d.ts +19 -0
  86. package/dist/node_modules/simple-git/dist/src/lib/responses/TagList.d.ts +7 -0
  87. package/dist/node_modules/simple-git/dist/src/lib/runners/git-executor-chain.d.ts +25 -0
  88. package/dist/node_modules/simple-git/dist/src/lib/runners/git-executor.d.ts +14 -0
  89. package/dist/node_modules/simple-git/dist/src/lib/runners/promise-wrapped.d.ts +2 -0
  90. package/dist/node_modules/simple-git/dist/src/lib/runners/scheduler.d.ts +11 -0
  91. package/dist/node_modules/simple-git/dist/src/lib/runners/tasks-pending-queue.d.ts +23 -0
  92. package/dist/node_modules/simple-git/dist/src/lib/simple-git-api.d.ts +20 -0
  93. package/dist/node_modules/simple-git/dist/src/lib/task-callback.d.ts +2 -0
  94. package/dist/node_modules/simple-git/dist/src/lib/tasks/apply-patch.d.ts +3 -0
  95. package/dist/node_modules/simple-git/dist/src/lib/tasks/branch.d.ts +7 -0
  96. package/dist/node_modules/simple-git/dist/src/lib/tasks/change-working-directory.d.ts +2 -0
  97. package/dist/node_modules/simple-git/dist/src/lib/tasks/check-ignore.d.ts +2 -0
  98. package/dist/node_modules/simple-git/dist/src/lib/tasks/check-is-repo.d.ts +9 -0
  99. package/dist/node_modules/simple-git/dist/src/lib/tasks/checkout.d.ts +2 -0
  100. package/dist/node_modules/simple-git/dist/src/lib/tasks/clean.d.ts +25 -0
  101. package/dist/node_modules/simple-git/dist/src/lib/tasks/clone.d.ts +9 -0
  102. package/dist/node_modules/simple-git/dist/src/lib/tasks/commit.d.ts +4 -0
  103. package/dist/node_modules/simple-git/dist/src/lib/tasks/config.d.ts +8 -0
  104. package/dist/node_modules/simple-git/dist/src/lib/tasks/count-objects.d.ts +12 -0
  105. package/dist/node_modules/simple-git/dist/src/lib/tasks/diff-name-status.d.ts +12 -0
  106. package/dist/node_modules/simple-git/dist/src/lib/tasks/diff.d.ts +5 -0
  107. package/dist/node_modules/simple-git/dist/src/lib/tasks/fetch.d.ts +4 -0
  108. package/dist/node_modules/simple-git/dist/src/lib/tasks/first-commit.d.ts +2 -0
  109. package/dist/node_modules/simple-git/dist/src/lib/tasks/grep.d.ts +12 -0
  110. package/dist/node_modules/simple-git/dist/src/lib/tasks/hash-object.d.ts +5 -0
  111. package/dist/node_modules/simple-git/dist/src/lib/tasks/init.d.ts +3 -0
  112. package/dist/node_modules/simple-git/dist/src/lib/tasks/log.d.ts +32 -0
  113. package/dist/node_modules/simple-git/dist/src/lib/tasks/merge.d.ts +4 -0
  114. package/dist/node_modules/simple-git/dist/src/lib/tasks/move.d.ts +3 -0
  115. package/dist/node_modules/simple-git/dist/src/lib/tasks/pull.d.ts +3 -0
  116. package/dist/node_modules/simple-git/dist/src/lib/tasks/push.d.ts +9 -0
  117. package/dist/node_modules/simple-git/dist/src/lib/tasks/remote.d.ts +8 -0
  118. package/dist/node_modules/simple-git/dist/src/lib/tasks/reset.d.ts +11 -0
  119. package/dist/node_modules/simple-git/dist/src/lib/tasks/show.d.ts +2 -0
  120. package/dist/node_modules/simple-git/dist/src/lib/tasks/stash-list.d.ts +4 -0
  121. package/dist/node_modules/simple-git/dist/src/lib/tasks/status.d.ts +3 -0
  122. package/dist/node_modules/simple-git/dist/src/lib/tasks/sub-module.d.ts +5 -0
  123. package/dist/node_modules/simple-git/dist/src/lib/tasks/tag.d.ts +18 -0
  124. package/dist/node_modules/simple-git/dist/src/lib/tasks/task.d.ts +14 -0
  125. package/dist/node_modules/simple-git/dist/src/lib/tasks/version.d.ts +9 -0
  126. package/dist/node_modules/simple-git/dist/src/lib/types/handlers.d.ts +21 -0
  127. package/dist/node_modules/simple-git/dist/src/lib/types/index.d.ts +136 -0
  128. package/dist/node_modules/simple-git/dist/src/lib/types/tasks.d.ts +19 -0
  129. package/dist/node_modules/simple-git/dist/src/lib/utils/argument-filters.d.ts +14 -0
  130. package/dist/node_modules/simple-git/dist/src/lib/utils/exit-codes.d.ts +10 -0
  131. package/dist/node_modules/simple-git/dist/src/lib/utils/git-output-streams.d.ts +7 -0
  132. package/dist/node_modules/simple-git/dist/src/lib/utils/index.d.ts +8 -0
  133. package/dist/node_modules/simple-git/dist/src/lib/utils/line-parser.d.ts +15 -0
  134. package/dist/node_modules/simple-git/dist/src/lib/utils/simple-git-options.d.ts +2 -0
  135. package/dist/node_modules/simple-git/dist/src/lib/utils/task-options.d.ts +13 -0
  136. package/dist/node_modules/simple-git/dist/src/lib/utils/task-parser.d.ts +5 -0
  137. package/dist/node_modules/simple-git/dist/src/lib/utils/util.d.ts +47 -0
  138. package/dist/node_modules/simple-git/dist/typings/errors.d.ts +5 -0
  139. package/dist/node_modules/simple-git/dist/typings/index.d.ts +14 -0
  140. package/dist/node_modules/simple-git/dist/typings/response.d.ts +556 -0
  141. package/dist/node_modules/simple-git/dist/typings/simple-git.d.ts +1033 -0
  142. package/dist/node_modules/simple-git/dist/typings/types.d.ts +22 -0
  143. package/dist/node_modules/simple-git/node_modules/debug/package.json +64 -0
  144. package/dist/node_modules/simple-git/node_modules/debug/src/browser.js +272 -0
  145. package/dist/node_modules/simple-git/node_modules/debug/src/common.js +292 -0
  146. package/dist/node_modules/simple-git/node_modules/debug/src/index.js +10 -0
  147. package/dist/node_modules/simple-git/node_modules/debug/src/node.js +263 -0
  148. package/dist/node_modules/simple-git/package.json +1 -0
  149. package/dist/node_modules/simple-git/promise.js +17 -0
  150. package/dist/server/collections/agent-execution-spans.d.ts +9 -0
  151. package/dist/server/collections/agent-execution-spans.js +152 -0
  152. package/dist/server/collections/orchestrator-config.js +16 -0
  153. package/dist/server/collections/orchestrator-logs.js +19 -2
  154. package/dist/server/collections/skill-definitions.d.ts +3 -0
  155. package/dist/server/collections/skill-definitions.js +158 -0
  156. package/dist/server/collections/skill-executions.d.ts +3 -0
  157. package/dist/server/collections/skill-executions.js +123 -0
  158. package/dist/server/collections/skill-worker-configs.d.ts +3 -0
  159. package/dist/server/collections/skill-worker-configs.js +115 -0
  160. package/dist/server/migrations/20260423000000-add-progress-fields.d.ts +4 -0
  161. package/dist/server/migrations/20260423000000-add-progress-fields.js +69 -0
  162. package/dist/server/migrations/20260425000000-add-interaction-schema.d.ts +4 -0
  163. package/dist/server/migrations/20260425000000-add-interaction-schema.js +61 -0
  164. package/dist/server/migrations/20260427000000-add-tracing-detail-fields.d.ts +7 -0
  165. package/dist/server/migrations/20260427000000-add-tracing-detail-fields.js +62 -0
  166. package/dist/server/migrations/20260427000000-change-packages-to-text.d.ts +4 -0
  167. package/dist/server/migrations/20260427000000-change-packages-to-text.js +70 -0
  168. package/dist/server/migrations/20260427000001-change-other-json-to-text.d.ts +4 -0
  169. package/dist/server/migrations/20260427000001-change-other-json-to-text.js +80 -0
  170. package/dist/server/migrations/20260429000000-add-llm-fields.d.ts +7 -0
  171. package/dist/server/migrations/20260429000000-add-llm-fields.js +68 -0
  172. package/dist/server/migrations/20260429000000-fix-inputargs-json-to-text.d.ts +16 -0
  173. package/dist/server/migrations/20260429000000-fix-inputargs-json-to-text.js +51 -0
  174. package/dist/server/migrations/20260503000000-add-orchestrator-trace-fields.d.ts +7 -0
  175. package/dist/server/migrations/20260503000000-add-orchestrator-trace-fields.js +57 -0
  176. package/dist/server/plugin.d.ts +3 -0
  177. package/dist/server/plugin.js +37 -1
  178. package/dist/server/resources/tracing.js +160 -12
  179. package/dist/server/services/CodeValidator.d.ts +32 -0
  180. package/dist/server/services/CodeValidator.js +205 -0
  181. package/dist/server/services/ExecutionSpanService.d.ts +44 -0
  182. package/dist/server/services/ExecutionSpanService.js +104 -0
  183. package/dist/server/services/FileManager.d.ts +28 -0
  184. package/dist/server/services/FileManager.js +151 -0
  185. package/dist/server/services/SandboxRunner.d.ts +41 -0
  186. package/dist/server/services/SandboxRunner.js +167 -0
  187. package/dist/server/services/SkillManager.d.ts +6 -0
  188. package/dist/server/services/SkillManager.js +640 -0
  189. package/dist/server/services/SkillRepositoryService.d.ts +22 -0
  190. package/dist/server/services/SkillRepositoryService.js +157 -0
  191. package/dist/server/services/WorkerEnvManager.d.ts +26 -0
  192. package/dist/server/services/WorkerEnvManager.js +120 -0
  193. package/dist/server/skill-hub/actions/git-import.d.ts +21 -0
  194. package/dist/server/skill-hub/actions/git-import.js +413 -0
  195. package/dist/server/skill-hub/mcp/McpController.d.ts +15 -0
  196. package/dist/server/skill-hub/mcp/McpController.js +111 -0
  197. package/dist/server/skill-hub/plugin.d.ts +58 -0
  198. package/dist/server/skill-hub/plugin.js +694 -0
  199. package/dist/server/skill-hub/sandbox-config.json +6 -0
  200. package/dist/server/skill-hub/tasks/SkillExecutionTask.d.ts +14 -0
  201. package/dist/server/skill-hub/tasks/SkillExecutionTask.js +267 -0
  202. package/dist/server/skill-hub/utils/json-fields.d.ts +7 -0
  203. package/dist/server/skill-hub/utils/json-fields.js +88 -0
  204. package/dist/server/tools/delegate-task.d.ts +4 -0
  205. package/dist/server/tools/delegate-task.js +832 -119
  206. package/dist/server/tools/skill-execute.d.ts +36 -0
  207. package/dist/server/tools/skill-execute.js +167 -0
  208. package/package.json +3 -1
  209. package/src/client/AIEmployeeSelect.tsx +1 -3
  210. package/src/client/AIEmployeesContext.tsx +28 -13
  211. package/src/client/OrchestratorSettings.tsx +43 -5
  212. package/src/client/RulesTab.tsx +368 -21
  213. package/src/client/TracingTab.tsx +316 -102
  214. package/src/client/plugin.tsx +39 -0
  215. package/src/client/skill-hub/components/ExecutionHistory.tsx +201 -0
  216. package/src/client/skill-hub/components/ExecutionProgress.tsx +55 -0
  217. package/src/client/skill-hub/components/GitSkillImport.tsx +555 -0
  218. package/src/client/skill-hub/components/SkillEditor.tsx +456 -0
  219. package/src/client/skill-hub/components/SkillManager.tsx +181 -0
  220. package/src/client/skill-hub/components/SkillMetrics.tsx +124 -0
  221. package/src/client/skill-hub/components/SkillTestPanel.tsx +144 -0
  222. package/src/client/skill-hub/index.tsx +75 -0
  223. package/src/client/skill-hub/locale.ts +16 -0
  224. package/src/client/skill-hub/tools/InteractionSchemasProvider.tsx +59 -0
  225. package/src/client/skill-hub/tools/SkillHubCard.tsx +78 -0
  226. package/src/client/skill-hub/utils/jsonFields.ts +37 -0
  227. package/src/server/collections/agent-execution-spans.ts +129 -0
  228. package/src/server/collections/orchestrator-config.ts +17 -0
  229. package/src/server/collections/orchestrator-logs.ts +19 -2
  230. package/src/server/collections/skill-definitions.ts +128 -0
  231. package/src/server/collections/skill-executions.ts +94 -0
  232. package/src/server/collections/skill-worker-configs.ts +86 -0
  233. package/src/server/migrations/20260423000000-add-progress-fields.ts +50 -0
  234. package/src/server/migrations/20260425000000-add-interaction-schema.ts +35 -0
  235. package/src/server/migrations/20260427000000-add-tracing-detail-fields.ts +41 -0
  236. package/src/server/migrations/20260427000000-change-packages-to-text.ts +47 -0
  237. package/src/server/migrations/20260427000001-change-other-json-to-text.ts +57 -0
  238. package/src/server/migrations/20260429000000-add-llm-fields.ts +46 -0
  239. package/src/server/migrations/20260429000000-fix-inputargs-json-to-text.ts +38 -0
  240. package/src/server/migrations/20260503000000-add-orchestrator-trace-fields.ts +32 -0
  241. package/src/server/plugin.ts +51 -3
  242. package/src/server/resources/tracing.ts +187 -16
  243. package/src/server/services/CodeValidator.ts +159 -0
  244. package/src/server/services/ExecutionSpanService.ts +106 -0
  245. package/src/server/services/FileManager.ts +144 -0
  246. package/src/server/services/SandboxRunner.ts +205 -0
  247. package/src/server/services/SkillManager.ts +623 -0
  248. package/src/server/services/SkillRepositoryService.ts +142 -0
  249. package/src/server/services/WorkerEnvManager.ts +113 -0
  250. package/src/server/skill-hub/actions/git-import.ts +486 -0
  251. package/src/server/skill-hub/mcp/McpController.ts +86 -0
  252. package/src/server/skill-hub/plugin.ts +771 -0
  253. package/src/server/skill-hub/sandbox-config.json +6 -0
  254. package/src/server/skill-hub/tasks/SkillExecutionTask.ts +297 -0
  255. package/src/server/skill-hub/utils/json-fields.ts +57 -0
  256. package/src/server/tools/delegate-task.ts +1085 -147
  257. 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
- const configRepo = plugin.db.getRepository('orchestratorConfig');
34
- if (!configRepo) return;
35
-
36
- const configs = await configRepo.find({
37
- filter: { enabled: true },
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
- // De-duplicate: register one tool per sub-agent (not per config row)
54
- const seenSubAgents = new Set<string>();
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 { leaderUsername, subAgentUsername, subAgentEmployee, task, context, maxDepth, timeout, toolCallId } = options;
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
- const modelSettings = subAgentEmployee.modelSettings;
179
- if (!modelSettings?.llmService || !modelSettings?.model) {
180
- throw new Error(`Sub-agent "${subAgentUsername}" has no LLM model configured. Please configure a model in the AI Employee settings.`);
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: string[] = (subAgentEmployee.skillSettings?.skills ?? [])
198
- .map((s: any) => (typeof s === 'string' ? s : s.name))
199
- .filter(Boolean);
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 toolName = toolEntry.definition.name;
205
- if (!toolName) continue;
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 delegate_to_* tools to prevent circular delegation
936
+ // Also skip our own orchestration tools to prevent circular delegation
209
937
  // (belt-and-suspenders with the depth check above).
210
- if (!employeeSkills.includes(toolName) || toolName.startsWith('delegate_to_')) {
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: toolName.replace(/[^a-zA-Z0-9_-]/g, '_'),
217
- description: toolEntry.definition.description || toolName,
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
- const res = await toolEntry.invoke(invokeCtx, toolArgs, `orch-${toolCallId}`);
225
- if (res?.status === 'error') {
226
- throw new Error(`Tool <${toolName}> failed: ${res.content}`);
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 = subAgentEmployee.chatSettings?.systemPrompt
243
- || subAgentEmployee.bio
244
- || `You are an AI assistant named "${subAgentEmployee.nickname || subAgentUsername}". ${subAgentEmployee.about || ''}`;
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 invokePromise = executeAgent(executor, systemPrompt, combinedTask, abortController.signal);
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 result = await Promise.race([
256
- invokePromise,
257
- createTimeout(timeout, subAgentUsername, abortController),
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 = (result as string) || 'Sub-agent completed the task but produced no output.';
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
- }).catch(() => {}); // Don't let logging errors mask the real error
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) return;
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
- await logsRepo.create({
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
- leaderUsername: data.leaderUsername,
323
- subAgentUsername: data.subAgentUsername,
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
- ): Promise<string> {
350
- const config: any = { recursionLimit: 50 };
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
- return lastAIMessage.content;
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
- if (Array.isArray(lastAIMessage.content)) {
377
- return lastAIMessage.content
378
- .map((c: any) => c.text || JSON.stringify(c))
379
- .join('\n');
380
- }
1291
+ return { content, messages: serializeMessages(messages) };
1292
+ }
381
1293
 
382
- return String(lastAIMessage.content);
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
- * Create a timeout promise that rejects after the specified duration.
387
- * P3 FIX: Also triggers AbortController to cancel the in-flight stream.
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(ms: number, agentName: string, abortController?: AbortController): Promise<never> {
390
- return new Promise((_, reject) =>
391
- setTimeout(() => {
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
  }