plugin-agent-orchestrator 1.0.19 → 1.0.21

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 (100) hide show
  1. package/dist/client/hooks/useRunEventStream.d.ts +22 -0
  2. package/dist/client/index.d.ts +1 -0
  3. package/dist/client/index.js +1 -1
  4. package/dist/externalVersion.js +6 -6
  5. package/dist/server/collections/agent-execution-spans.js +24 -0
  6. package/dist/server/collections/agent-loop-runs.js +36 -0
  7. package/dist/server/collections/orchestrator-config.js +14 -0
  8. package/dist/server/migrations/20260601000000-add-token-fields.d.ts +7 -0
  9. package/dist/server/migrations/20260601000000-add-token-fields.js +101 -0
  10. package/dist/server/plugin.js +47 -0
  11. package/dist/server/resources/agent-loop.js +33 -25
  12. package/dist/server/resources/tracing.js +5 -8
  13. package/dist/server/services/AgentHarness.d.ts +2 -0
  14. package/dist/server/services/AgentHarness.js +56 -90
  15. package/dist/server/services/AgentLoopController.d.ts +33 -20
  16. package/dist/server/services/AgentLoopController.js +164 -125
  17. package/dist/server/services/AgentLoopRepository.js +16 -34
  18. package/dist/server/services/AgentLoopService.d.ts +28 -18
  19. package/dist/server/services/AgentLoopService.js +7 -1
  20. package/dist/server/services/AgentPlannerService.js +5 -25
  21. package/dist/server/services/AgentRegistryService.d.ts +8 -0
  22. package/dist/server/services/AgentRegistryService.js +34 -24
  23. package/dist/server/services/CircuitBreaker.d.ts +40 -0
  24. package/dist/server/services/CircuitBreaker.js +120 -0
  25. package/dist/server/services/ContextAggregator.d.ts +45 -0
  26. package/dist/server/services/ContextAggregator.js +201 -0
  27. package/dist/server/services/ExecutionSpanService.js +2 -5
  28. package/dist/server/services/RunEventBus.d.ts +9 -0
  29. package/dist/server/services/RunEventBus.js +73 -0
  30. package/dist/server/services/TokenTracker.d.ts +62 -0
  31. package/dist/server/services/TokenTracker.js +173 -0
  32. package/dist/server/skill-hub/plugin.js +6 -6
  33. package/dist/server/skill-hub/tasks/SkillExecutionTask.js +6 -6
  34. package/dist/server/tools/agent-loop.d.ts +8 -8
  35. package/dist/server/tools/agent-loop.js +30 -63
  36. package/dist/server/tools/delegate-task.js +14 -72
  37. package/dist/server/tools/orchestrator-plan.d.ts +6 -6
  38. package/dist/server/tools/orchestrator-plan.js +10 -47
  39. package/dist/server/types.d.ts +47 -0
  40. package/dist/server/types.js +24 -0
  41. package/dist/server/utils/ctx-utils.d.ts +30 -0
  42. package/dist/server/utils/ctx-utils.js +152 -0
  43. package/dist/server/utils/logging.d.ts +6 -0
  44. package/dist/server/utils/logging.js +86 -0
  45. package/package.json +44 -44
  46. package/src/client/AgentRunsTab.tsx +764 -764
  47. package/src/client/HarnessProfilesTab.tsx +247 -247
  48. package/src/client/OrchestratorSettings.tsx +106 -106
  49. package/src/client/RulesTab.tsx +716 -716
  50. package/src/client/hooks/useRunEventStream.ts +76 -0
  51. package/src/client/index.tsx +2 -1
  52. package/src/client/plugin.tsx +27 -27
  53. package/src/client/skill-hub/components/LoopSettings.tsx +331 -331
  54. package/src/client/skill-hub/index.tsx +51 -51
  55. package/src/client/skill-hub/tools/InteractionSchemasProvider.tsx +99 -99
  56. package/src/client/skill-hub/tools/SkillHubCard.tsx +109 -109
  57. package/src/client/skill-hub/tools/loopTemplates.ts +52 -52
  58. package/src/client/skill-hub/tools/registerSkillLoopCards.ts +58 -58
  59. package/src/client/tools/PlanApprovalCard.tsx +175 -175
  60. package/src/client/tools/registerOrchestratorCards.ts +7 -7
  61. package/src/server/__tests__/agent-loop-controller.test.ts +375 -0
  62. package/src/server/__tests__/circuit-breaker.test.ts +169 -0
  63. package/src/server/__tests__/context-aggregator.test.ts +222 -0
  64. package/src/server/__tests__/parallel-execution.test.ts +318 -0
  65. package/src/server/__tests__/smoke.test.ts +120 -0
  66. package/src/server/collections/agent-execution-spans.ts +24 -0
  67. package/src/server/collections/agent-harness-profiles.ts +59 -59
  68. package/src/server/collections/agent-loop-events.ts +71 -71
  69. package/src/server/collections/agent-loop-runs.ts +38 -1
  70. package/src/server/collections/agent-loop-steps.ts +144 -144
  71. package/src/server/collections/orchestrator-config.ts +14 -0
  72. package/src/server/collections/skill-executions.ts +106 -106
  73. package/src/server/collections/skill-loop-configs.ts +65 -65
  74. package/src/server/migrations/20260524000000-add-agent-loop-fields-to-skill-executions.ts +30 -30
  75. package/src/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.ts +142 -142
  76. package/src/server/migrations/20260601000000-add-token-fields.ts +89 -0
  77. package/src/server/plugin.ts +53 -0
  78. package/src/server/resources/agent-loop.ts +21 -12
  79. package/src/server/resources/tracing.ts +3 -7
  80. package/src/server/services/AgentHarness.ts +78 -116
  81. package/src/server/services/AgentLoopController.ts +197 -122
  82. package/src/server/services/AgentLoopRepository.ts +9 -25
  83. package/src/server/services/AgentLoopService.ts +13 -1
  84. package/src/server/services/AgentPlanValidator.ts +73 -73
  85. package/src/server/services/AgentPlannerService.ts +2 -25
  86. package/src/server/services/AgentRegistryService.ts +40 -31
  87. package/src/server/services/CircuitBreaker.ts +116 -0
  88. package/src/server/services/ContextAggregator.ts +239 -0
  89. package/src/server/services/ExecutionSpanService.ts +2 -4
  90. package/src/server/services/RunEventBus.ts +45 -0
  91. package/src/server/services/TokenTracker.ts +209 -0
  92. package/src/server/skill-hub/plugin.ts +898 -897
  93. package/src/server/skill-hub/tasks/SkillExecutionTask.ts +460 -458
  94. package/src/server/tools/agent-loop.ts +18 -57
  95. package/src/server/tools/delegate-task.ts +11 -93
  96. package/src/server/tools/orchestrator-plan.ts +26 -50
  97. package/src/server/tools/skill-execute.ts +160 -160
  98. package/src/server/types.ts +55 -0
  99. package/src/server/utils/ctx-utils.ts +118 -0
  100. package/src/server/utils/logging.ts +63 -0
@@ -1,142 +1,142 @@
1
- import { DataTypes } from '@nocobase/database';
2
- import { Migration } from '@nocobase/server';
3
-
4
- export default class AddPlanApprovalAndHarnessProfiles extends Migration {
5
- on = 'afterLoad';
6
- appVersion = '>=0.1.0';
7
-
8
- async up() {
9
- const db = (this as any).db;
10
- const queryInterface = db.sequelize.getQueryInterface();
11
- const tablePrefix = db.options.tablePrefix || '';
12
-
13
- await this.addRunColumns(queryInterface, `${tablePrefix}agentLoopRuns`);
14
- await this.addStepColumns(queryInterface, `${tablePrefix}agentLoopSteps`);
15
- await this.addConfigColumns(queryInterface, `${tablePrefix}orchestratorConfig`);
16
- await this.ensureHarnessProfiles(queryInterface, `${tablePrefix}agentHarnessProfiles`);
17
- await this.seedDefaultProfiles();
18
- }
19
-
20
- async addRunColumns(queryInterface: any, tableName: string) {
21
- const tableExists = await queryInterface.tableExists(tableName).catch(() => false);
22
- if (!tableExists) return;
23
- const tableDesc = await queryInterface.describeTable(tableName);
24
- const addIfMissing = async (name: string, spec: any) => {
25
- if (tableDesc[name]) return;
26
- await queryInterface.addColumn(tableName, name, spec);
27
- };
28
-
29
- await addIfMissing('approvalStatus', { type: DataTypes.STRING(30), allowNull: true, defaultValue: 'none' });
30
- await addIfMissing('approvedById', { type: DataTypes.BIGINT, allowNull: true });
31
- await addIfMissing('approvedAt', { type: DataTypes.DATE, allowNull: true });
32
- await addIfMissing('rejectionReason', { type: DataTypes.TEXT, allowNull: true });
33
- await addIfMissing('changeRequest', { type: DataTypes.TEXT, allowNull: true });
34
- await addIfMissing('planVersion', { type: DataTypes.INTEGER, allowNull: true, defaultValue: 1 });
35
- await addIfMissing('planSource', { type: DataTypes.STRING(50), allowNull: true });
36
- await addIfMissing('plannerModel', { type: DataTypes.STRING(100), allowNull: true });
37
- await addIfMissing('lockedBy', { type: DataTypes.STRING(100), allowNull: true });
38
- await addIfMissing('lockedUntil', { type: DataTypes.DATE, allowNull: true });
39
- }
40
-
41
- async addStepColumns(queryInterface: any, tableName: string) {
42
- const tableExists = await queryInterface.tableExists(tableName).catch(() => false);
43
- if (!tableExists) return;
44
- const tableDesc = await queryInterface.describeTable(tableName);
45
- if (!tableDesc.dependencyPolicy) {
46
- await queryInterface.addColumn(tableName, 'dependencyPolicy', {
47
- type: DataTypes.STRING(30),
48
- allowNull: true,
49
- defaultValue: 'require_success',
50
- });
51
- }
52
- }
53
-
54
- async addConfigColumns(queryInterface: any, tableName: string) {
55
- const tableExists = await queryInterface.tableExists(tableName).catch(() => false);
56
- if (!tableExists) return;
57
- const tableDesc = await queryInterface.describeTable(tableName);
58
- if (!tableDesc.harnessTag) {
59
- await queryInterface.addColumn(tableName, 'harnessTag', {
60
- type: DataTypes.STRING(100),
61
- allowNull: true,
62
- defaultValue: 'default',
63
- });
64
- }
65
- }
66
-
67
- async ensureHarnessProfiles(queryInterface: any, tableName: string) {
68
- const tableExists = await queryInterface.tableExists(tableName).catch(() => false);
69
- if (tableExists) return;
70
- await queryInterface.createTable(tableName, {
71
- id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
72
- tag: { type: DataTypes.STRING(100), allowNull: false, unique: true },
73
- title: { type: DataTypes.STRING(200), allowNull: true },
74
- description: { type: DataTypes.TEXT, allowNull: true },
75
- enabled: { type: DataTypes.BOOLEAN, allowNull: true, defaultValue: true },
76
- settings: { type: DataTypes.JSON, allowNull: true, defaultValue: {} },
77
- createdAt: { type: DataTypes.DATE, allowNull: true },
78
- updatedAt: { type: DataTypes.DATE, allowNull: true },
79
- });
80
- }
81
-
82
- async seedDefaultProfiles() {
83
- const repo = (this as any).db.getRepository('agentHarnessProfiles');
84
- if (!repo) return;
85
- const profiles = [
86
- {
87
- tag: 'default',
88
- title: 'Default',
89
- description: 'Balanced profile for normal multi-agent work.',
90
- settings: {
91
- requirePlanApproval: true,
92
- allowSubAgents: true,
93
- allowToolCalls: true,
94
- maxParallelSubAgents: 3,
95
- maxControllerSteps: 100,
96
- },
97
- },
98
- {
99
- tag: 'safe',
100
- title: 'Safe',
101
- description: 'Strict approval-first profile for higher-risk work.',
102
- settings: {
103
- requirePlanApproval: true,
104
- allowSubAgents: true,
105
- allowToolCalls: true,
106
- maxParallelSubAgents: 1,
107
- maxControllerSteps: 50,
108
- requireVerification: true,
109
- },
110
- },
111
- {
112
- tag: 'file-heavy',
113
- title: 'File Heavy',
114
- description: 'Profile for tasks that inspect or transform many attachments/files.',
115
- settings: {
116
- requirePlanApproval: true,
117
- allowSubAgents: true,
118
- allowToolCalls: true,
119
- maxParallelSubAgents: 2,
120
- maxControllerSteps: 120,
121
- preferFileTools: true,
122
- },
123
- },
124
- ];
125
- for (const profile of profiles) {
126
- const existing = await repo.findOne({ filter: { tag: profile.tag } });
127
- if (existing) continue;
128
- await repo.create({
129
- values: {
130
- ...profile,
131
- enabled: true,
132
- createdAt: new Date(),
133
- updatedAt: new Date(),
134
- },
135
- });
136
- }
137
- }
138
-
139
- async down() {
140
- // No rollback: new nullable columns and the profile table are backward compatible.
141
- }
142
- }
1
+ import { DataTypes } from '@nocobase/database';
2
+ import { Migration } from '@nocobase/server';
3
+
4
+ export default class AddPlanApprovalAndHarnessProfiles extends Migration {
5
+ on = 'afterLoad';
6
+ appVersion = '>=0.1.0';
7
+
8
+ async up() {
9
+ const db = (this as any).db;
10
+ const queryInterface = db.sequelize.getQueryInterface();
11
+ const tablePrefix = db.options.tablePrefix || '';
12
+
13
+ await this.addRunColumns(queryInterface, `${tablePrefix}agentLoopRuns`);
14
+ await this.addStepColumns(queryInterface, `${tablePrefix}agentLoopSteps`);
15
+ await this.addConfigColumns(queryInterface, `${tablePrefix}orchestratorConfig`);
16
+ await this.ensureHarnessProfiles(queryInterface, `${tablePrefix}agentHarnessProfiles`);
17
+ await this.seedDefaultProfiles();
18
+ }
19
+
20
+ async addRunColumns(queryInterface: any, tableName: string) {
21
+ const tableExists = await queryInterface.tableExists(tableName).catch(() => false);
22
+ if (!tableExists) return;
23
+ const tableDesc = await queryInterface.describeTable(tableName);
24
+ const addIfMissing = async (name: string, spec: any) => {
25
+ if (tableDesc[name]) return;
26
+ await queryInterface.addColumn(tableName, name, spec);
27
+ };
28
+
29
+ await addIfMissing('approvalStatus', { type: DataTypes.STRING(30), allowNull: true, defaultValue: 'none' });
30
+ await addIfMissing('approvedById', { type: DataTypes.BIGINT, allowNull: true });
31
+ await addIfMissing('approvedAt', { type: DataTypes.DATE, allowNull: true });
32
+ await addIfMissing('rejectionReason', { type: DataTypes.TEXT, allowNull: true });
33
+ await addIfMissing('changeRequest', { type: DataTypes.TEXT, allowNull: true });
34
+ await addIfMissing('planVersion', { type: DataTypes.INTEGER, allowNull: true, defaultValue: 1 });
35
+ await addIfMissing('planSource', { type: DataTypes.STRING(50), allowNull: true });
36
+ await addIfMissing('plannerModel', { type: DataTypes.STRING(100), allowNull: true });
37
+ await addIfMissing('lockedBy', { type: DataTypes.STRING(100), allowNull: true });
38
+ await addIfMissing('lockedUntil', { type: DataTypes.DATE, allowNull: true });
39
+ }
40
+
41
+ async addStepColumns(queryInterface: any, tableName: string) {
42
+ const tableExists = await queryInterface.tableExists(tableName).catch(() => false);
43
+ if (!tableExists) return;
44
+ const tableDesc = await queryInterface.describeTable(tableName);
45
+ if (!tableDesc.dependencyPolicy) {
46
+ await queryInterface.addColumn(tableName, 'dependencyPolicy', {
47
+ type: DataTypes.STRING(30),
48
+ allowNull: true,
49
+ defaultValue: 'require_success',
50
+ });
51
+ }
52
+ }
53
+
54
+ async addConfigColumns(queryInterface: any, tableName: string) {
55
+ const tableExists = await queryInterface.tableExists(tableName).catch(() => false);
56
+ if (!tableExists) return;
57
+ const tableDesc = await queryInterface.describeTable(tableName);
58
+ if (!tableDesc.harnessTag) {
59
+ await queryInterface.addColumn(tableName, 'harnessTag', {
60
+ type: DataTypes.STRING(100),
61
+ allowNull: true,
62
+ defaultValue: 'default',
63
+ });
64
+ }
65
+ }
66
+
67
+ async ensureHarnessProfiles(queryInterface: any, tableName: string) {
68
+ const tableExists = await queryInterface.tableExists(tableName).catch(() => false);
69
+ if (tableExists) return;
70
+ await queryInterface.createTable(tableName, {
71
+ id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
72
+ tag: { type: DataTypes.STRING(100), allowNull: false, unique: true },
73
+ title: { type: DataTypes.STRING(200), allowNull: true },
74
+ description: { type: DataTypes.TEXT, allowNull: true },
75
+ enabled: { type: DataTypes.BOOLEAN, allowNull: true, defaultValue: true },
76
+ settings: { type: DataTypes.JSON, allowNull: true, defaultValue: {} },
77
+ createdAt: { type: DataTypes.DATE, allowNull: true },
78
+ updatedAt: { type: DataTypes.DATE, allowNull: true },
79
+ });
80
+ }
81
+
82
+ async seedDefaultProfiles() {
83
+ const repo = (this as any).db.getRepository('agentHarnessProfiles');
84
+ if (!repo) return;
85
+ const profiles = [
86
+ {
87
+ tag: 'default',
88
+ title: 'Default',
89
+ description: 'Balanced profile for normal multi-agent work.',
90
+ settings: {
91
+ requirePlanApproval: true,
92
+ allowSubAgents: true,
93
+ allowToolCalls: true,
94
+ maxParallelSubAgents: 3,
95
+ maxControllerSteps: 100,
96
+ },
97
+ },
98
+ {
99
+ tag: 'safe',
100
+ title: 'Safe',
101
+ description: 'Strict approval-first profile for higher-risk work.',
102
+ settings: {
103
+ requirePlanApproval: true,
104
+ allowSubAgents: true,
105
+ allowToolCalls: true,
106
+ maxParallelSubAgents: 1,
107
+ maxControllerSteps: 50,
108
+ requireVerification: true,
109
+ },
110
+ },
111
+ {
112
+ tag: 'file-heavy',
113
+ title: 'File Heavy',
114
+ description: 'Profile for tasks that inspect or transform many attachments/files.',
115
+ settings: {
116
+ requirePlanApproval: true,
117
+ allowSubAgents: true,
118
+ allowToolCalls: true,
119
+ maxParallelSubAgents: 2,
120
+ maxControllerSteps: 120,
121
+ preferFileTools: true,
122
+ },
123
+ },
124
+ ];
125
+ for (const profile of profiles) {
126
+ const existing = await repo.findOne({ filter: { tag: profile.tag } });
127
+ if (existing) continue;
128
+ await repo.create({
129
+ values: {
130
+ ...profile,
131
+ enabled: true,
132
+ createdAt: new Date(),
133
+ updatedAt: new Date(),
134
+ },
135
+ });
136
+ }
137
+ }
138
+
139
+ async down() {
140
+ // No rollback: new nullable columns and the profile table are backward compatible.
141
+ }
142
+ }
@@ -0,0 +1,89 @@
1
+ import { Migration } from '@nocobase/server';
2
+
3
+ export default class AddTokenFieldsMigration extends Migration {
4
+ on = 'afterLoad';
5
+ appVersion = '<=2.x';
6
+
7
+ async up() {
8
+ const queryInterface = (this as any).db.sequelize.getQueryInterface();
9
+ const DataTypes = (this as any).db.sequelize.constructor['DataTypes'];
10
+ const prefix = (this as any).db.options.tablePrefix || '';
11
+
12
+ // ── agentExecutionSpans ──
13
+ const spansTable = `${prefix}agentExecutionSpans`;
14
+ const spansExists = await queryInterface
15
+ .describeTable(spansTable)
16
+ .then(() => true)
17
+ .catch(() => false);
18
+ if (spansExists) {
19
+ const spansCols = await queryInterface.describeTable(spansTable);
20
+ const addSpanIfMissing = async (name: string, definition: any) => {
21
+ if (!spansCols[name]) {
22
+ await queryInterface.addColumn(spansTable, name, definition);
23
+ }
24
+ };
25
+ await addSpanIfMissing('inputTokens', { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 });
26
+ await addSpanIfMissing('outputTokens', { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 });
27
+ await addSpanIfMissing('totalTokens', { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 });
28
+ await addSpanIfMissing('cost', { type: DataTypes.FLOAT, allowNull: false, defaultValue: 0 });
29
+ }
30
+
31
+ // ── agentLoopRuns ──
32
+ const runsTable = `${prefix}agentLoopRuns`;
33
+ const runsExists = await queryInterface
34
+ .describeTable(runsTable)
35
+ .then(() => true)
36
+ .catch(() => false);
37
+ if (runsExists) {
38
+ const runsCols = await queryInterface.describeTable(runsTable);
39
+ const addRunIfMissing = async (name: string, definition: any) => {
40
+ if (!runsCols[name]) {
41
+ await queryInterface.addColumn(runsTable, name, definition);
42
+ }
43
+ };
44
+ await addRunIfMissing('totalInputTokens', { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 });
45
+ await addRunIfMissing('totalOutputTokens', { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 });
46
+ await addRunIfMissing('totalTokens', { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 });
47
+ await addRunIfMissing('totalCost', { type: DataTypes.FLOAT, allowNull: false, defaultValue: 0 });
48
+ await addRunIfMissing('budgetMaxTokens', { type: DataTypes.INTEGER, allowNull: true });
49
+ await addRunIfMissing('budgetMaxCost', { type: DataTypes.FLOAT, allowNull: true });
50
+ }
51
+
52
+ // ── orchestratorConfig ──
53
+ const configTable = `${prefix}orchestratorConfig`;
54
+ const configExists = await queryInterface
55
+ .describeTable(configTable)
56
+ .then(() => true)
57
+ .catch(() => false);
58
+ if (configExists) {
59
+ const configCols = await queryInterface.describeTable(configTable);
60
+ const addConfigIfMissing = async (name: string, definition: any) => {
61
+ if (!configCols[name]) {
62
+ await queryInterface.addColumn(configTable, name, definition);
63
+ }
64
+ };
65
+ await addConfigIfMissing('budgetMaxTokens', { type: DataTypes.INTEGER, allowNull: true, defaultValue: 0 });
66
+ await addConfigIfMissing('budgetMaxCost', { type: DataTypes.FLOAT, allowNull: true, defaultValue: 0 });
67
+ }
68
+ }
69
+
70
+ async down() {
71
+ const queryInterface = (this as any).db.sequelize.getQueryInterface();
72
+ const prefix = (this as any).db.options.tablePrefix || '';
73
+
74
+ const tables = [
75
+ { name: `${prefix}agentExecutionSpans`, cols: ['inputTokens', 'outputTokens', 'totalTokens', 'cost'] },
76
+ {
77
+ name: `${prefix}agentLoopRuns`,
78
+ cols: ['totalInputTokens', 'totalOutputTokens', 'totalTokens', 'totalCost', 'budgetMaxTokens', 'budgetMaxCost'],
79
+ },
80
+ { name: `${prefix}orchestratorConfig`, cols: ['budgetMaxTokens', 'budgetMaxCost'] },
81
+ ];
82
+
83
+ for (const { name, cols } of tables) {
84
+ for (const col of cols) {
85
+ await queryInterface.removeColumn(name, col).catch(() => {});
86
+ }
87
+ }
88
+ }
89
+ }
@@ -5,6 +5,7 @@ import { createExternalRagSearchTool } from './tools/external-rag-search';
5
5
  import { createOrchestratorPlanTools } from './tools/orchestrator-plan';
6
6
  import { registerTracingResource } from './resources/tracing';
7
7
  import { registerAgentLoopResource } from './resources/agent-loop';
8
+ import { getRunEventBus } from './services/RunEventBus';
8
9
  import SkillHubSubFeature from './skill-hub/plugin';
9
10
  import { AgentLoopService } from './services/AgentLoopService';
10
11
 
@@ -41,6 +42,7 @@ export class PluginAgentOrchestratorServer extends Plugin {
41
42
  'agentLoopRuns:*',
42
43
  'agentLoopSteps:*',
43
44
  'agentLoopEvents:*',
45
+ 'agentLoopEventsStream:*',
44
46
  'agentHarnessProfiles:*',
45
47
  'agentExecutionSpans:*',
46
48
  'skillDefinitions:*',
@@ -63,6 +65,57 @@ export class PluginAgentOrchestratorServer extends Plugin {
63
65
  // --- Register Agent Loop Resource ---
64
66
  registerAgentLoopResource(this, this.agentLoopService);
65
67
 
68
+ // --- Register SSE Event Stream Resource (Phase 6) ---
69
+ (this as any).app.resource({
70
+ name: 'agentLoopEventsStream',
71
+ actions: {
72
+ async stream(ctx, next) {
73
+ const runId = ctx.action.params?.runId || ctx.query?.runId || ctx.request.query?.runId;
74
+ if (!runId) {
75
+ ctx.throw(400, 'runId query parameter is required.');
76
+ return;
77
+ }
78
+
79
+ ctx.type = 'text/event-stream';
80
+ ctx.set('Cache-Control', 'no-cache');
81
+ ctx.set('Connection', 'keep-alive');
82
+ ctx.set('X-Accel-Buffering', 'no');
83
+
84
+ const unsubscribe = getRunEventBus().subscribe(runId, (event: any) => {
85
+ try {
86
+ ctx.res.write(`data: ${JSON.stringify(event)}\n\n`);
87
+ } catch {
88
+ unsubscribe();
89
+ }
90
+ });
91
+
92
+ const keepalive = setInterval(() => {
93
+ try {
94
+ ctx.res.write(': keepalive\n\n');
95
+ } catch {
96
+ clearInterval(keepalive);
97
+ unsubscribe();
98
+ }
99
+ }, 15000);
100
+
101
+ ctx.req.on('close', () => {
102
+ clearInterval(keepalive);
103
+ unsubscribe();
104
+ });
105
+
106
+ ctx.req.on('error', () => {
107
+ clearInterval(keepalive);
108
+ unsubscribe();
109
+ });
110
+
111
+ ctx.res.writeHead(200);
112
+ ctx.res.write(': connected\n\n');
113
+
114
+ await next();
115
+ },
116
+ },
117
+ });
118
+
66
119
  // --- Register Tracing Resource (Phase 5) ---
67
120
  // Custom read-only resource for the Swarm Tracing admin page.
68
121
  registerTracingResource(this);
@@ -1,17 +1,6 @@
1
1
  import { Plugin } from '@nocobase/server';
2
2
  import { AgentLoopService } from '../services/AgentLoopService';
3
-
4
- function toPlain(record: any) {
5
- return record?.toJSON?.() || record;
6
- }
7
-
8
- function currentUserId(ctx: any) {
9
- return ctx?.state?.currentUser?.id || ctx?.auth?.user?.id;
10
- }
11
-
12
- function values(ctx: any) {
13
- return ctx.request?.body || ctx.action?.params?.values || {};
14
- }
3
+ import { toPlain, currentUserId, valuesFromCtx as values } from '../utils/ctx-utils';
15
4
 
16
5
  function formatRunRow(raw: any) {
17
6
  const row = toPlain(raw);
@@ -178,6 +167,26 @@ export function registerAgentLoopResource(plugin: Plugin, service: AgentLoopServ
178
167
  };
179
168
  await next();
180
169
  },
170
+
171
+ async stepFeedback(ctx, next) {
172
+ const body = values(ctx);
173
+ if (!body.stepId || !body.rating) {
174
+ ctx.throw(400, 'stepId and rating are required');
175
+ return;
176
+ }
177
+ if (!['positive', 'negative'].includes(body.rating)) {
178
+ ctx.throw(400, 'rating must be "positive" or "negative"');
179
+ return;
180
+ }
181
+ ctx.body = {
182
+ data: await service.stepFeedback(
183
+ body.stepId,
184
+ { rating: body.rating, comment: body.comment, category: body.category },
185
+ { userId: currentUserId(ctx) },
186
+ ),
187
+ };
188
+ await next();
189
+ },
181
190
  },
182
191
  });
183
192
  }
@@ -1,8 +1,6 @@
1
1
  import { Plugin } from '@nocobase/server';
2
2
 
3
- function toPlain(row: any) {
4
- return row?.toJSON?.() || row;
5
- }
3
+ import { toPlain } from '../utils/ctx-utils';
6
4
 
7
5
  function normalizeSpanFilter(filter: any = {}) {
8
6
  const next = { ...filter };
@@ -68,8 +66,7 @@ function buildSpanTree(rows: any[]) {
68
66
  const sortTree = (items: any[]) => {
69
67
  items.sort(
70
68
  (a, b) =>
71
- new Date(a.startedAt || a.createdAt || 0).getTime() -
72
- new Date(b.startedAt || b.createdAt || 0).getTime(),
69
+ new Date(a.startedAt || a.createdAt || 0).getTime() - new Date(b.startedAt || b.createdAt || 0).getTime(),
73
70
  );
74
71
  for (const item of items) sortTree(item.children || []);
75
72
  };
@@ -82,8 +79,7 @@ function flattenSpanTimeline(rows: any[]) {
82
79
  .map(toPlain)
83
80
  .sort(
84
81
  (a, b) =>
85
- new Date(a.startedAt || a.createdAt || 0).getTime() -
86
- new Date(b.startedAt || b.createdAt || 0).getTime(),
82
+ new Date(a.startedAt || a.createdAt || 0).getTime() - new Date(b.startedAt || b.createdAt || 0).getTime(),
87
83
  )
88
84
  .map((row) => {
89
85
  const input = row.input || {};