plugin-agent-orchestrator 1.0.16 → 1.0.18

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 (92) hide show
  1. package/dist/client/AgentRunsTab.d.ts +2 -0
  2. package/dist/client/HarnessProfilesTab.d.ts +2 -0
  3. package/dist/client/index.js +1 -1
  4. package/dist/client/skill-hub/components/LoopSettings.d.ts +2 -0
  5. package/dist/client/skill-hub/index.d.ts +2 -1
  6. package/dist/client/skill-hub/tools/InteractionSchemasProvider.d.ts +1 -14
  7. package/dist/client/skill-hub/tools/loopTemplates.d.ts +22 -0
  8. package/dist/client/skill-hub/tools/registerSkillLoopCards.d.ts +1 -0
  9. package/dist/client/tools/PlanApprovalCard.d.ts +3 -0
  10. package/dist/client/tools/registerOrchestratorCards.d.ts +1 -0
  11. package/dist/externalVersion.js +6 -6
  12. package/dist/server/collections/agent-harness-profiles.d.ts +2 -0
  13. package/dist/server/collections/agent-harness-profiles.js +89 -0
  14. package/dist/server/collections/agent-loop-events.d.ts +2 -0
  15. package/dist/server/collections/agent-loop-events.js +101 -0
  16. package/dist/server/collections/agent-loop-runs.d.ts +2 -0
  17. package/dist/server/collections/agent-loop-runs.js +188 -0
  18. package/dist/server/collections/agent-loop-steps.d.ts +2 -0
  19. package/dist/server/collections/agent-loop-steps.js +174 -0
  20. package/dist/server/collections/orchestrator-config.js +7 -0
  21. package/dist/server/collections/skill-executions.js +12 -0
  22. package/dist/server/collections/skill-loop-configs.d.ts +3 -0
  23. package/dist/server/collections/skill-loop-configs.js +94 -0
  24. package/dist/server/migrations/20260524000000-add-agent-loop-fields-to-skill-executions.d.ts +7 -0
  25. package/dist/server/migrations/20260524000000-add-agent-loop-fields-to-skill-executions.js +55 -0
  26. package/dist/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.d.ts +12 -0
  27. package/dist/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.js +162 -0
  28. package/dist/server/plugin.d.ts +2 -0
  29. package/dist/server/plugin.js +13 -0
  30. package/dist/server/resources/agent-loop.d.ts +3 -0
  31. package/dist/server/resources/agent-loop.js +205 -0
  32. package/dist/server/services/AgentHarness.d.ts +42 -0
  33. package/dist/server/services/AgentHarness.js +565 -0
  34. package/dist/server/services/AgentLoopController.d.ts +205 -0
  35. package/dist/server/services/AgentLoopController.js +940 -0
  36. package/dist/server/services/AgentLoopRepository.d.ts +20 -0
  37. package/dist/server/services/AgentLoopRepository.js +210 -0
  38. package/dist/server/services/AgentLoopService.d.ts +149 -0
  39. package/dist/server/services/AgentLoopService.js +133 -0
  40. package/dist/server/services/AgentPlanValidator.d.ts +4 -0
  41. package/dist/server/services/AgentPlanValidator.js +99 -0
  42. package/dist/server/services/AgentPlannerService.d.ts +8 -0
  43. package/dist/server/services/AgentPlannerService.js +119 -0
  44. package/dist/server/services/AgentRegistryService.d.ts +13 -0
  45. package/dist/server/services/AgentRegistryService.js +178 -0
  46. package/dist/server/services/ExecutionSpanService.d.ts +2 -0
  47. package/dist/server/skill-hub/plugin.d.ts +3 -0
  48. package/dist/server/skill-hub/plugin.js +137 -54
  49. package/dist/server/tools/agent-loop.d.ts +235 -0
  50. package/dist/server/tools/agent-loop.js +406 -0
  51. package/dist/server/tools/delegate-task.js +37 -350
  52. package/dist/server/tools/orchestrator-plan.d.ts +205 -0
  53. package/dist/server/tools/orchestrator-plan.js +291 -0
  54. package/dist/server/tools/skill-execute.js +2 -0
  55. package/package.json +2 -2
  56. package/src/client/AgentRunsTab.tsx +764 -0
  57. package/src/client/HarnessProfilesTab.tsx +247 -0
  58. package/src/client/OrchestratorSettings.tsx +40 -2
  59. package/src/client/RulesTab.tsx +103 -6
  60. package/src/client/plugin.tsx +27 -54
  61. package/src/client/skill-hub/components/LoopSettings.tsx +331 -0
  62. package/src/client/skill-hub/index.tsx +51 -75
  63. package/src/client/skill-hub/tools/InteractionSchemasProvider.tsx +56 -16
  64. package/src/client/skill-hub/tools/SkillHubCard.tsx +35 -4
  65. package/src/client/skill-hub/tools/loopTemplates.ts +52 -0
  66. package/src/client/skill-hub/tools/registerSkillLoopCards.ts +58 -0
  67. package/src/client/tools/PlanApprovalCard.tsx +175 -0
  68. package/src/client/tools/registerOrchestratorCards.ts +7 -0
  69. package/src/server/collections/agent-harness-profiles.ts +59 -0
  70. package/src/server/collections/agent-loop-events.ts +71 -0
  71. package/src/server/collections/agent-loop-runs.ts +158 -0
  72. package/src/server/collections/agent-loop-steps.ts +144 -0
  73. package/src/server/collections/orchestrator-config.ts +7 -0
  74. package/src/server/collections/skill-executions.ts +63 -51
  75. package/src/server/collections/skill-loop-configs.ts +65 -0
  76. package/src/server/migrations/20260524000000-add-agent-loop-fields-to-skill-executions.ts +30 -0
  77. package/src/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.ts +142 -0
  78. package/src/server/plugin.ts +15 -0
  79. package/src/server/resources/agent-loop.ts +183 -0
  80. package/src/server/services/AgentHarness.ts +663 -0
  81. package/src/server/services/AgentLoopController.ts +1128 -0
  82. package/src/server/services/AgentLoopRepository.ts +194 -0
  83. package/src/server/services/AgentLoopService.ts +161 -0
  84. package/src/server/services/AgentPlanValidator.ts +73 -0
  85. package/src/server/services/AgentPlannerService.ts +93 -0
  86. package/src/server/services/AgentRegistryService.ts +169 -0
  87. package/src/server/services/ExecutionSpanService.ts +2 -0
  88. package/src/server/skill-hub/plugin.ts +881 -771
  89. package/src/server/tools/agent-loop.ts +399 -0
  90. package/src/server/tools/delegate-task.ts +48 -463
  91. package/src/server/tools/orchestrator-plan.ts +279 -0
  92. package/src/server/tools/skill-execute.ts +68 -64
@@ -18,6 +18,11 @@ import {
18
18
  * Used to prevent circular/recursive delegation chains.
19
19
  */
20
20
  const ORCHESTRATOR_DEPTH_KEY = '__orchestratorDepth';
21
+ /**
22
+ * Context key for tracking the full delegation path.
23
+ * Used to detect and prevent circular delegation chains.
24
+ */
25
+ const ORCHESTRATOR_PATH_KEY = '__orchestratorPath';
21
26
 
22
27
  /** Max sub-agents that the dispatch tool runs concurrently in one call. */
23
28
  const MAX_DISPATCH_CONCURRENCY = 5;
@@ -59,10 +64,7 @@ function buildDispatchToolName(leaderUsername: string) {
59
64
  }
60
65
 
61
66
  function createRootRunId(seed = '') {
62
- const hash = createHash('sha1')
63
- .update(`${Date.now()}::${Math.random()}::${seed}`)
64
- .digest('hex')
65
- .slice(0, 10);
67
+ const hash = createHash('sha1').update(`${Date.now()}::${Math.random()}::${seed}`).digest('hex').slice(0, 10);
66
68
  return `run_${Date.now()}_${hash}`;
67
69
  }
68
70
 
@@ -142,7 +144,7 @@ function createDelegateToolOptions(
142
144
  return {
143
145
  scope: 'CUSTOM',
144
146
  execution: 'backend',
145
- defaultPermission: 'ALLOW',
147
+ defaultPermission: 'ASK',
146
148
  silence: false,
147
149
  introduction: {
148
150
  title: `[${leaderUsername}] ${subAgentEmployee.nickname || subAgentUsername}${legacyAlias ? ' (legacy)' : ''}`,
@@ -289,7 +291,7 @@ function createDispatchToolOptions(
289
291
  return {
290
292
  scope: 'CUSTOM',
291
293
  execution: 'backend',
292
- defaultPermission: 'ALLOW',
294
+ defaultPermission: 'ASK',
293
295
  silence: false,
294
296
  introduction: {
295
297
  title: `[${leaderUsername}] Dispatch sub-agents`,
@@ -788,15 +790,14 @@ async function invokeDelegateTask(
788
790
  } = options;
789
791
 
790
792
  // --- 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
793
  const ctxSnapshot = captureCtxSnapshot(ctx);
796
794
 
797
- // --- P1: Depth enforcement ---
795
+ // --- Depth enforcement & Circular Delegation Detection ---
798
796
  const currentDepth: number = (ctx as any)[ORCHESTRATOR_DEPTH_KEY] ?? 0;
799
- if (currentDepth >= maxDepth) {
797
+ const currentPath: string[] = (ctx as any)[ORCHESTRATOR_PATH_KEY] ?? [leaderUsername];
798
+
799
+ if (currentPath.includes(subAgentUsername)) {
800
+ const loopChain = [...currentPath, subAgentUsername].join(' -> ');
800
801
  await logDelegation(ctx, plugin, {
801
802
  leaderUsername,
802
803
  subAgentUsername,
@@ -807,347 +808,17 @@ async function invokeDelegateTask(
807
808
  status: 'error',
808
809
  depth: currentDepth,
809
810
  durationMs: 0,
810
- error: `Delegation depth limit reached (${currentDepth}/${maxDepth}).`,
811
+ error: `Circular delegation detected: ${loopChain}.`,
811
812
  snapshot: ctxSnapshot,
812
813
  });
813
814
  return {
814
815
  status: 'error' as const,
815
- content: `Delegation depth limit reached (${currentDepth}/${maxDepth}). Sub-agent "${subAgentUsername}" cannot delegate further.`,
816
+ content: `Circular delegation detected: ${loopChain}. Execution aborted to prevent infinite reasoning loops.`,
816
817
  };
817
818
  }
818
819
 
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;
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
- }
868
-
869
- try {
870
- const aiPlugin = ctx.app.pm.get('ai') as PluginAIServer;
871
- if (!aiPlugin) {
872
- throw new Error('Plugin AI is not installed or enabled');
873
- }
874
-
875
- // --- Step 1: Resolve LLM model from sub-agent's employee config ---
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
- }
903
- }
904
-
905
- const { provider } = await aiPlugin.aiManager.getLLMService({
906
- llmService: modelSettings.llmService,
907
- model: modelSettings.model,
908
- });
909
- const chatModel = provider.createModel();
910
-
911
- // --- Step 2: Resolve tools via CORE toolsManager ---
912
- // Uses app.aiManager.toolsManager (same as AIEmployee.getToolsMap at ai-employee.ts:1286)
913
- // NOT plugin-ai's local toolManager (which has a different grouped format).
914
- const coreToolsManager = ctx.app.aiManager.toolsManager;
915
- const allTools: ToolsEntry[] = await coreToolsManager.listTools();
916
-
917
- // skillSettings.skills is { name: string, autoCall: boolean }[]
918
- // (verified at ai-employee.ts:1028-1029)
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]));
927
-
928
- const langchainTools: DynamicStructuredTool[] = [];
929
-
930
- for (const toolEntry of allTools) {
931
- const entryName = toolEntry.definition.name;
932
- if (!entryName) continue;
933
- const employeeSkill = employeeSkillMap.get(entryName);
934
-
935
- // Only include tools that the sub-agent employee is configured to use.
936
- // Also skip our own orchestration tools to prevent circular delegation
937
- // (belt-and-suspenders with the depth check above).
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
- ) {
949
- continue;
950
- }
951
-
952
- langchainTools.push(
953
- new DynamicStructuredTool({
954
- name: entryName.replace(/[^a-zA-Z0-9_-]/g, '_'),
955
- description: toolEntry.definition.description || entryName,
956
- schema: (toolEntry.definition.schema || z.object({})) as any,
957
- func: async (toolArgs) => {
958
- // Forward the invoke with depth tracking
959
- const invokeCtx = Object.create(ctx);
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
- });
998
-
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;
1035
- }
1036
- },
1037
- }),
1038
- );
1039
- }
1040
-
1041
- // --- Step 3: Build the agent ---
1042
- const abortController = new AbortController();
1043
- const executor = createReactAgent({
1044
- llm: chatModel,
1045
- tools: langchainTools,
1046
- });
1047
-
1048
- // --- Step 4: Construct messages ---
1049
- let systemPrompt =
1050
- subAgentEmployee.chatSettings?.systemPrompt ||
1051
- subAgentEmployee.bio ||
1052
- `You are an AI assistant named "${subAgentEmployee.nickname || subAgentUsername}". ${
1053
- subAgentEmployee.about || ''
1054
- }`;
1055
-
1056
- // --- Step 4b: Inject shared context from Knowledge Base (soft dependency) ---
1057
- // If plugin-knowledge-base is installed, inject the session context summary
1058
- // so the sub-agent is aware of findings from previous agents in this run.
1059
- try {
1060
- const kbPlugin = ctx.app.pm.get('plugin-knowledge-base') as any;
1061
- if (kbPlugin?.sessionContext) {
1062
- const sessionId =
1063
- ctx.action?.params?.values?.sessionId ||
1064
- ctx.action?.params?.sessionId ||
1065
- ctx.state?.sessionId;
1066
-
1067
- const contextSummary = await kbPlugin.sessionContext.buildSummary(
1068
- { rootRunId, ...(sessionId ? { sessionId } : {}) },
1069
- 6000,
1070
- );
1071
- if (contextSummary) {
1072
- systemPrompt += `\n\n<shared_context>\nThe following context was shared by other agents in this workflow. Use it to avoid redundant work:\n${contextSummary}\n</shared_context>`;
1073
- }
1074
- }
1075
- } catch (e: any) {
1076
- // Graceful fallback — never block delegation due to context injection failure.
1077
- ctx.app.log?.debug?.(`[AgentOrchestrator] Shared context injection skipped: ${e.message}`);
1078
- }
1079
-
1080
- const combinedTask = context ? `Task: ${task}\n\nContext Provided:\n${context}` : `Task: ${task}`;
1081
-
1082
- // --- Step 5: Execute with timeout + abort ---
1083
- // P3 FIX: AbortController signal cancels the in-flight stream on timeout,
1084
- // preventing continued token consumption after the timeout fires.
1085
- const effectiveRecursionLimit =
1086
- Number.isFinite(recursionLimit) && (recursionLimit as number) > 0 ? (recursionLimit as number) : 50;
1087
- const invokePromise = executeAgent(
1088
- executor,
1089
- systemPrompt,
1090
- combinedTask,
1091
- abortController.signal,
1092
- effectiveRecursionLimit,
1093
- );
1094
-
1095
- const timeoutHandle = createTimeout(timeout, subAgentUsername, abortController);
1096
- let result: AgentExecutionResult;
1097
- try {
1098
- result = (await Promise.race([invokePromise, timeoutHandle.promise])) as AgentExecutionResult;
1099
- } finally {
1100
- // Always release the timer so it doesn't keep the event loop alive.
1101
- timeoutHandle.cancel();
1102
- }
1103
-
1104
- const content = result.content || 'Sub-agent completed the task but produced no output.';
1105
- trace.push({
1106
- type: 'finish',
1107
- at: nowIso(),
1108
- title: `Delegation finished: ${subAgentUsername}`,
1109
- status: 'success',
1110
- content: truncateText(content, 2000),
1111
- });
1112
-
1113
- // Log successful execution for tracing
1114
- await logDelegation(ctx, plugin, {
1115
- id: logRecord?.id,
1116
- leaderUsername,
1117
- subAgentUsername,
1118
- toolName,
1119
- task,
1120
- context,
1121
- result: content,
1122
- status: 'success',
1123
- depth: currentDepth,
1124
- durationMs: Date.now() - startTime,
1125
- trace,
1126
- messages: result.messages,
1127
- snapshot: ctxSnapshot,
1128
- });
1129
- await spanService.finish(executionSpanId, 'success', startTime, {
1130
- output: content,
1131
- metadata: {
1132
- depth: currentDepth,
1133
- maxDepth,
1134
- toolName,
1135
- recursionLimit,
1136
- messages: result.messages,
1137
- traceCount: trace.length,
1138
- },
1139
- });
1140
-
1141
- return {
1142
- status: 'success' as const,
1143
- content,
1144
- };
1145
- } catch (e) {
1146
- plugin.app.log.error(`[AgentOrchestrator] Sub-agent ${subAgentUsername} failed`, e);
1147
-
1148
- // Log failed execution for tracing
820
+ if (currentDepth >= maxDepth) {
1149
821
  await logDelegation(ctx, plugin, {
1150
- id: logRecord?.id,
1151
822
  leaderUsername,
1152
823
  subAgentUsername,
1153
824
  toolName,
@@ -1156,38 +827,43 @@ async function invokeDelegateTask(
1156
827
  result: '',
1157
828
  status: 'error',
1158
829
  depth: currentDepth,
1159
- durationMs: Date.now() - startTime,
1160
- error: e.message,
1161
- trace: [
1162
- ...trace,
1163
- {
1164
- type: 'error',
1165
- at: nowIso(),
1166
- title: `Delegation failed: ${subAgentUsername}`,
1167
- status: 'error',
1168
- content: e.message,
1169
- },
1170
- ],
830
+ durationMs: 0,
831
+ error: `Delegation depth limit reached (${currentDepth}/${maxDepth}).`,
1171
832
  snapshot: ctxSnapshot,
1172
- }).catch((logErr) => {
1173
- plugin.app.log.warn('[AgentOrchestrator] Failed to save error log for delegation', logErr);
1174
- });
1175
- await spanService.finish(executionSpanId, 'error', startTime, {
1176
- error: truncateText(e.message, 10000),
1177
- metadata: {
1178
- depth: currentDepth,
1179
- maxDepth,
1180
- toolName,
1181
- recursionLimit,
1182
- traceCount: trace.length + 1,
1183
- },
1184
833
  });
1185
-
1186
834
  return {
1187
835
  status: 'error' as const,
1188
- content: `Sub-agent "${subAgentUsername}" failed: ${e.message}`,
836
+ content: `Delegation depth limit reached (${currentDepth}/${maxDepth}). Sub-agent "${subAgentUsername}" cannot delegate further.`,
1189
837
  };
1190
838
  }
839
+
840
+ const upstreamTraceContext = getOrchestratorTraceContext(ctx);
841
+ const rootRunId =
842
+ providedRootRunId || upstreamTraceContext?.rootRunId || createRootRunId(`${leaderUsername}:${subAgentUsername}`);
843
+ const parentSpanId = providedParentSpanId || upstreamTraceContext?.spanId || upstreamTraceContext?.parentSpanId;
844
+ const agentLoopRunId = upstreamTraceContext?.agentLoopRunId;
845
+ const agentLoopStepId = upstreamTraceContext?.agentLoopStepId;
846
+
847
+ return plugin.agentLoopService.harness.runSubAgent(ctx, {
848
+ leaderUsername,
849
+ subAgentUsername,
850
+ subAgentEmployee,
851
+ task,
852
+ context,
853
+ currentDepth,
854
+ currentPath,
855
+ maxDepth,
856
+ timeout,
857
+ toolCallId,
858
+ toolName,
859
+ llmService,
860
+ model,
861
+ recursionLimit,
862
+ rootRunId,
863
+ parentSpanId,
864
+ agentLoopRunId,
865
+ agentLoopStepId,
866
+ });
1191
867
  }
1192
868
 
1193
869
  /**
@@ -1220,15 +896,12 @@ async function logDelegation(
1220
896
  return;
1221
897
  }
1222
898
 
1223
- // Prefer the early snapshot captured in invokeDelegateTask — by the time
1224
- // the agent finishes, ctx may already be disposed. Fall back to ctx for
1225
- // call sites that don't pass a snapshot (e.g. authz-failure short-circuit).
1226
899
  let userId: number | undefined = data.snapshot?.userId;
1227
900
  if (userId == null) {
1228
901
  try {
1229
902
  userId = ctx.auth?.user?.id || ctx.state?.currentUser?.id;
1230
903
  } catch {
1231
- // ctx lifecycle ended — proceed without userId
904
+ // ctx lifecycle ended
1232
905
  }
1233
906
  }
1234
907
 
@@ -1268,91 +941,3 @@ async function logDelegation(
1268
941
  plugin.app.log.warn('[AgentOrchestrator] Failed to log delegation event', e);
1269
942
  }
1270
943
  }
1271
-
1272
- /**
1273
- * Execute the agent and extract the final AI message content.
1274
- * Uses executor.invoke to get the final state cleanly, avoiding chunk parsing issues.
1275
- * Accepts an AbortSignal so the execution can be cancelled on timeout.
1276
- */
1277
- async function executeAgent(
1278
- executor: any,
1279
- systemPrompt: string,
1280
- task: string,
1281
- signal?: AbortSignal,
1282
- recursionLimit = 50,
1283
- ): Promise<AgentExecutionResult> {
1284
- const config: any = { recursionLimit };
1285
- if (signal) {
1286
- config.signal = signal;
1287
- }
1288
-
1289
- const finalState = await executor.invoke(
1290
- {
1291
- messages: [new SystemMessage(systemPrompt), new HumanMessage(task)],
1292
- },
1293
- config,
1294
- );
1295
-
1296
- // finalState.messages contains the entire conversation history of this delegation
1297
- const messages = finalState?.messages || [];
1298
-
1299
- // Find the last AI message in the chain
1300
- const lastAIMessage = [...messages].reverse().find((m) => m.getType() === 'ai');
1301
-
1302
- if (!lastAIMessage || !lastAIMessage.content) {
1303
- return { content: '', messages: serializeMessages(messages) };
1304
- }
1305
-
1306
- let content = '';
1307
- if (typeof lastAIMessage.content === 'string') {
1308
- content = lastAIMessage.content;
1309
- } else if (Array.isArray(lastAIMessage.content)) {
1310
- content = lastAIMessage.content.map((c: any) => c.text || JSON.stringify(c)).join('\n');
1311
- } else {
1312
- content = String(lastAIMessage.content);
1313
- }
1314
-
1315
- return { content, messages: serializeMessages(messages) };
1316
- }
1317
-
1318
- function serializeMessages(messages: any[]) {
1319
- return (messages || []).map((message, index) => {
1320
- const type = typeof message.getType === 'function' ? message.getType() : message.type;
1321
- return {
1322
- index,
1323
- type,
1324
- name: message.name,
1325
- content: truncateText(message.content, 10000),
1326
- toolCalls: message.tool_calls || message.toolCalls || [],
1327
- toolCallId: message.tool_call_id,
1328
- additionalKwargs: message.additional_kwargs,
1329
- responseMetadata: message.response_metadata,
1330
- };
1331
- });
1332
- }
1333
-
1334
- /**
1335
- * Schedule a rejection-on-timeout that also aborts the in-flight stream.
1336
- * Returns the promise plus a `cancel()` so callers can release the timer
1337
- * when the race resolves successfully (otherwise the handle keeps the event
1338
- * loop alive until `ms` elapses).
1339
- */
1340
- function createTimeout(
1341
- ms: number,
1342
- agentName: string,
1343
- abortController?: AbortController,
1344
- ): { promise: Promise<never>; cancel: () => void } {
1345
- let timer: ReturnType<typeof setTimeout> | undefined;
1346
- const promise = new Promise<never>((_resolve, reject) => {
1347
- timer = setTimeout(() => {
1348
- abortController?.abort();
1349
- reject(new Error(`Sub-agent "${agentName}" timed out after ${ms / 1000}s`));
1350
- }, ms);
1351
- });
1352
- return {
1353
- promise,
1354
- cancel: () => {
1355
- if (timer) clearTimeout(timer);
1356
- },
1357
- };
1358
- }