plugin-agent-orchestrator 1.0.20 → 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 (98) 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/tools/agent-loop.d.ts +8 -8
  33. package/dist/server/tools/agent-loop.js +30 -63
  34. package/dist/server/tools/delegate-task.js +14 -72
  35. package/dist/server/tools/orchestrator-plan.d.ts +6 -6
  36. package/dist/server/tools/orchestrator-plan.js +10 -47
  37. package/dist/server/types.d.ts +47 -0
  38. package/dist/server/types.js +24 -0
  39. package/dist/server/utils/ctx-utils.d.ts +30 -0
  40. package/dist/server/utils/ctx-utils.js +152 -0
  41. package/dist/server/utils/logging.d.ts +6 -0
  42. package/dist/server/utils/logging.js +86 -0
  43. package/package.json +44 -44
  44. package/src/client/AgentRunsTab.tsx +764 -764
  45. package/src/client/HarnessProfilesTab.tsx +247 -247
  46. package/src/client/OrchestratorSettings.tsx +106 -106
  47. package/src/client/RulesTab.tsx +716 -716
  48. package/src/client/hooks/useRunEventStream.ts +76 -0
  49. package/src/client/index.tsx +2 -1
  50. package/src/client/plugin.tsx +27 -27
  51. package/src/client/skill-hub/components/LoopSettings.tsx +331 -331
  52. package/src/client/skill-hub/index.tsx +51 -51
  53. package/src/client/skill-hub/tools/InteractionSchemasProvider.tsx +99 -99
  54. package/src/client/skill-hub/tools/SkillHubCard.tsx +109 -109
  55. package/src/client/skill-hub/tools/loopTemplates.ts +52 -52
  56. package/src/client/skill-hub/tools/registerSkillLoopCards.ts +58 -58
  57. package/src/client/tools/PlanApprovalCard.tsx +175 -175
  58. package/src/client/tools/registerOrchestratorCards.ts +7 -7
  59. package/src/server/__tests__/agent-loop-controller.test.ts +375 -0
  60. package/src/server/__tests__/circuit-breaker.test.ts +169 -0
  61. package/src/server/__tests__/context-aggregator.test.ts +222 -0
  62. package/src/server/__tests__/parallel-execution.test.ts +318 -0
  63. package/src/server/__tests__/smoke.test.ts +120 -0
  64. package/src/server/collections/agent-execution-spans.ts +24 -0
  65. package/src/server/collections/agent-harness-profiles.ts +59 -59
  66. package/src/server/collections/agent-loop-events.ts +71 -71
  67. package/src/server/collections/agent-loop-runs.ts +38 -1
  68. package/src/server/collections/agent-loop-steps.ts +144 -144
  69. package/src/server/collections/orchestrator-config.ts +14 -0
  70. package/src/server/collections/skill-executions.ts +106 -106
  71. package/src/server/collections/skill-loop-configs.ts +65 -65
  72. package/src/server/migrations/20260524000000-add-agent-loop-fields-to-skill-executions.ts +30 -30
  73. package/src/server/migrations/20260524001000-add-plan-approval-and-harness-profiles.ts +142 -142
  74. package/src/server/migrations/20260601000000-add-token-fields.ts +89 -0
  75. package/src/server/plugin.ts +53 -0
  76. package/src/server/resources/agent-loop.ts +21 -12
  77. package/src/server/resources/tracing.ts +3 -7
  78. package/src/server/services/AgentHarness.ts +78 -116
  79. package/src/server/services/AgentLoopController.ts +197 -122
  80. package/src/server/services/AgentLoopRepository.ts +9 -25
  81. package/src/server/services/AgentLoopService.ts +13 -1
  82. package/src/server/services/AgentPlanValidator.ts +73 -73
  83. package/src/server/services/AgentPlannerService.ts +2 -25
  84. package/src/server/services/AgentRegistryService.ts +40 -31
  85. package/src/server/services/CircuitBreaker.ts +116 -0
  86. package/src/server/services/ContextAggregator.ts +239 -0
  87. package/src/server/services/ExecutionSpanService.ts +2 -4
  88. package/src/server/services/RunEventBus.ts +45 -0
  89. package/src/server/services/TokenTracker.ts +209 -0
  90. package/src/server/skill-hub/plugin.ts +898 -898
  91. package/src/server/skill-hub/tasks/SkillExecutionTask.ts +460 -460
  92. package/src/server/tools/agent-loop.ts +18 -57
  93. package/src/server/tools/delegate-task.ts +11 -93
  94. package/src/server/tools/orchestrator-plan.ts +26 -50
  95. package/src/server/tools/skill-execute.ts +160 -160
  96. package/src/server/types.ts +55 -0
  97. package/src/server/utils/ctx-utils.ts +118 -0
  98. 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 || {};