@generacy-ai/generacy 0.0.0-preview-20260304013206

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 (244) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +207 -0
  3. package/bin/generacy.js +11 -0
  4. package/dist/agency/index.d.ts +68 -0
  5. package/dist/agency/index.d.ts.map +1 -0
  6. package/dist/agency/index.js +28 -0
  7. package/dist/agency/index.js.map +1 -0
  8. package/dist/agency/network.d.ts +41 -0
  9. package/dist/agency/network.d.ts.map +1 -0
  10. package/dist/agency/network.js +133 -0
  11. package/dist/agency/network.js.map +1 -0
  12. package/dist/agency/subprocess.d.ts +58 -0
  13. package/dist/agency/subprocess.d.ts.map +1 -0
  14. package/dist/agency/subprocess.js +216 -0
  15. package/dist/agency/subprocess.js.map +1 -0
  16. package/dist/cli/commands/agent.d.ts +10 -0
  17. package/dist/cli/commands/agent.d.ts.map +1 -0
  18. package/dist/cli/commands/agent.js +216 -0
  19. package/dist/cli/commands/agent.js.map +1 -0
  20. package/dist/cli/commands/doctor/checks/agency-mcp.d.ts +3 -0
  21. package/dist/cli/commands/doctor/checks/agency-mcp.d.ts.map +1 -0
  22. package/dist/cli/commands/doctor/checks/agency-mcp.js +51 -0
  23. package/dist/cli/commands/doctor/checks/agency-mcp.js.map +1 -0
  24. package/dist/cli/commands/doctor/checks/anthropic-key.d.ts +3 -0
  25. package/dist/cli/commands/doctor/checks/anthropic-key.d.ts.map +1 -0
  26. package/dist/cli/commands/doctor/checks/anthropic-key.js +68 -0
  27. package/dist/cli/commands/doctor/checks/anthropic-key.js.map +1 -0
  28. package/dist/cli/commands/doctor/checks/config.d.ts +3 -0
  29. package/dist/cli/commands/doctor/checks/config.d.ts.map +1 -0
  30. package/dist/cli/commands/doctor/checks/config.js +81 -0
  31. package/dist/cli/commands/doctor/checks/config.js.map +1 -0
  32. package/dist/cli/commands/doctor/checks/devcontainer.d.ts +3 -0
  33. package/dist/cli/commands/doctor/checks/devcontainer.d.ts.map +1 -0
  34. package/dist/cli/commands/doctor/checks/devcontainer.js +58 -0
  35. package/dist/cli/commands/doctor/checks/devcontainer.js.map +1 -0
  36. package/dist/cli/commands/doctor/checks/docker.d.ts +3 -0
  37. package/dist/cli/commands/doctor/checks/docker.d.ts.map +1 -0
  38. package/dist/cli/commands/doctor/checks/docker.js +71 -0
  39. package/dist/cli/commands/doctor/checks/docker.js.map +1 -0
  40. package/dist/cli/commands/doctor/checks/env-file.d.ts +3 -0
  41. package/dist/cli/commands/doctor/checks/env-file.d.ts.map +1 -0
  42. package/dist/cli/commands/doctor/checks/env-file.js +56 -0
  43. package/dist/cli/commands/doctor/checks/env-file.js.map +1 -0
  44. package/dist/cli/commands/doctor/checks/github-token.d.ts +3 -0
  45. package/dist/cli/commands/doctor/checks/github-token.d.ts.map +1 -0
  46. package/dist/cli/commands/doctor/checks/github-token.js +99 -0
  47. package/dist/cli/commands/doctor/checks/github-token.js.map +1 -0
  48. package/dist/cli/commands/doctor/checks/npm-packages.d.ts +3 -0
  49. package/dist/cli/commands/doctor/checks/npm-packages.d.ts.map +1 -0
  50. package/dist/cli/commands/doctor/checks/npm-packages.js +117 -0
  51. package/dist/cli/commands/doctor/checks/npm-packages.js.map +1 -0
  52. package/dist/cli/commands/doctor/formatter.d.ts +27 -0
  53. package/dist/cli/commands/doctor/formatter.d.ts.map +1 -0
  54. package/dist/cli/commands/doctor/formatter.js +162 -0
  55. package/dist/cli/commands/doctor/formatter.js.map +1 -0
  56. package/dist/cli/commands/doctor/index.d.ts +5 -0
  57. package/dist/cli/commands/doctor/index.d.ts.map +1 -0
  58. package/dist/cli/commands/doctor/index.js +8 -0
  59. package/dist/cli/commands/doctor/index.js.map +1 -0
  60. package/dist/cli/commands/doctor/registry.d.ts +48 -0
  61. package/dist/cli/commands/doctor/registry.d.ts.map +1 -0
  62. package/dist/cli/commands/doctor/registry.js +166 -0
  63. package/dist/cli/commands/doctor/registry.js.map +1 -0
  64. package/dist/cli/commands/doctor/runner.d.ts +14 -0
  65. package/dist/cli/commands/doctor/runner.d.ts.map +1 -0
  66. package/dist/cli/commands/doctor/runner.js +257 -0
  67. package/dist/cli/commands/doctor/runner.js.map +1 -0
  68. package/dist/cli/commands/doctor/types.d.ts +87 -0
  69. package/dist/cli/commands/doctor/types.d.ts.map +1 -0
  70. package/dist/cli/commands/doctor/types.js +2 -0
  71. package/dist/cli/commands/doctor/types.js.map +1 -0
  72. package/dist/cli/commands/doctor.d.ts +12 -0
  73. package/dist/cli/commands/doctor.d.ts.map +1 -0
  74. package/dist/cli/commands/doctor.js +97 -0
  75. package/dist/cli/commands/doctor.js.map +1 -0
  76. package/dist/cli/commands/init/conflicts.d.ts +36 -0
  77. package/dist/cli/commands/init/conflicts.d.ts.map +1 -0
  78. package/dist/cli/commands/init/conflicts.js +165 -0
  79. package/dist/cli/commands/init/conflicts.js.map +1 -0
  80. package/dist/cli/commands/init/github.d.ts +32 -0
  81. package/dist/cli/commands/init/github.d.ts.map +1 -0
  82. package/dist/cli/commands/init/github.js +161 -0
  83. package/dist/cli/commands/init/github.js.map +1 -0
  84. package/dist/cli/commands/init/index.d.ts +21 -0
  85. package/dist/cli/commands/init/index.d.ts.map +1 -0
  86. package/dist/cli/commands/init/index.js +175 -0
  87. package/dist/cli/commands/init/index.js.map +1 -0
  88. package/dist/cli/commands/init/prompts.d.ts +15 -0
  89. package/dist/cli/commands/init/prompts.d.ts.map +1 -0
  90. package/dist/cli/commands/init/prompts.js +281 -0
  91. package/dist/cli/commands/init/prompts.js.map +1 -0
  92. package/dist/cli/commands/init/repo-utils.d.ts +32 -0
  93. package/dist/cli/commands/init/repo-utils.d.ts.map +1 -0
  94. package/dist/cli/commands/init/repo-utils.js +112 -0
  95. package/dist/cli/commands/init/repo-utils.js.map +1 -0
  96. package/dist/cli/commands/init/resolver.d.ts +20 -0
  97. package/dist/cli/commands/init/resolver.d.ts.map +1 -0
  98. package/dist/cli/commands/init/resolver.js +273 -0
  99. package/dist/cli/commands/init/resolver.js.map +1 -0
  100. package/dist/cli/commands/init/summary.d.ts +21 -0
  101. package/dist/cli/commands/init/summary.d.ts.map +1 -0
  102. package/dist/cli/commands/init/summary.js +100 -0
  103. package/dist/cli/commands/init/summary.js.map +1 -0
  104. package/dist/cli/commands/init/types.d.ts +53 -0
  105. package/dist/cli/commands/init/types.d.ts.map +1 -0
  106. package/dist/cli/commands/init/types.js +2 -0
  107. package/dist/cli/commands/init/types.js.map +1 -0
  108. package/dist/cli/commands/init/writer.d.ts +22 -0
  109. package/dist/cli/commands/init/writer.d.ts.map +1 -0
  110. package/dist/cli/commands/init/writer.js +96 -0
  111. package/dist/cli/commands/init/writer.js.map +1 -0
  112. package/dist/cli/commands/orchestrator.d.ts +11 -0
  113. package/dist/cli/commands/orchestrator.d.ts.map +1 -0
  114. package/dist/cli/commands/orchestrator.js +291 -0
  115. package/dist/cli/commands/orchestrator.js.map +1 -0
  116. package/dist/cli/commands/run.d.ts +10 -0
  117. package/dist/cli/commands/run.d.ts.map +1 -0
  118. package/dist/cli/commands/run.js +167 -0
  119. package/dist/cli/commands/run.js.map +1 -0
  120. package/dist/cli/commands/setup/auth.d.ts +11 -0
  121. package/dist/cli/commands/setup/auth.d.ts.map +1 -0
  122. package/dist/cli/commands/setup/auth.js +108 -0
  123. package/dist/cli/commands/setup/auth.js.map +1 -0
  124. package/dist/cli/commands/setup/build.d.ts +11 -0
  125. package/dist/cli/commands/setup/build.d.ts.map +1 -0
  126. package/dist/cli/commands/setup/build.js +212 -0
  127. package/dist/cli/commands/setup/build.js.map +1 -0
  128. package/dist/cli/commands/setup/services.d.ts +11 -0
  129. package/dist/cli/commands/setup/services.d.ts.map +1 -0
  130. package/dist/cli/commands/setup/services.js +294 -0
  131. package/dist/cli/commands/setup/services.js.map +1 -0
  132. package/dist/cli/commands/setup/workspace.d.ts +11 -0
  133. package/dist/cli/commands/setup/workspace.d.ts.map +1 -0
  134. package/dist/cli/commands/setup/workspace.js +215 -0
  135. package/dist/cli/commands/setup/workspace.js.map +1 -0
  136. package/dist/cli/commands/setup.d.ts +7 -0
  137. package/dist/cli/commands/setup.d.ts.map +1 -0
  138. package/dist/cli/commands/setup.js +19 -0
  139. package/dist/cli/commands/setup.js.map +1 -0
  140. package/dist/cli/commands/validate.d.ts +10 -0
  141. package/dist/cli/commands/validate.d.ts.map +1 -0
  142. package/dist/cli/commands/validate.js +164 -0
  143. package/dist/cli/commands/validate.js.map +1 -0
  144. package/dist/cli/commands/worker.d.ts +10 -0
  145. package/dist/cli/commands/worker.d.ts.map +1 -0
  146. package/dist/cli/commands/worker.js +224 -0
  147. package/dist/cli/commands/worker.js.map +1 -0
  148. package/dist/cli/index.d.ts +14 -0
  149. package/dist/cli/index.d.ts.map +1 -0
  150. package/dist/cli/index.js +68 -0
  151. package/dist/cli/index.js.map +1 -0
  152. package/dist/cli/utils/config.d.ts +49 -0
  153. package/dist/cli/utils/config.d.ts.map +1 -0
  154. package/dist/cli/utils/config.js +110 -0
  155. package/dist/cli/utils/config.js.map +1 -0
  156. package/dist/cli/utils/exec.d.ts +39 -0
  157. package/dist/cli/utils/exec.d.ts.map +1 -0
  158. package/dist/cli/utils/exec.js +68 -0
  159. package/dist/cli/utils/exec.js.map +1 -0
  160. package/dist/cli/utils/logger.d.ts +47 -0
  161. package/dist/cli/utils/logger.d.ts.map +1 -0
  162. package/dist/cli/utils/logger.js +97 -0
  163. package/dist/cli/utils/logger.js.map +1 -0
  164. package/dist/config/index.d.ts +10 -0
  165. package/dist/config/index.d.ts.map +1 -0
  166. package/dist/config/index.js +13 -0
  167. package/dist/config/index.js.map +1 -0
  168. package/dist/config/loader.d.ts +104 -0
  169. package/dist/config/loader.d.ts.map +1 -0
  170. package/dist/config/loader.js +266 -0
  171. package/dist/config/loader.js.map +1 -0
  172. package/dist/config/schema.d.ts +304 -0
  173. package/dist/config/schema.d.ts.map +1 -0
  174. package/dist/config/schema.js +160 -0
  175. package/dist/config/schema.js.map +1 -0
  176. package/dist/config/validator.d.ts +60 -0
  177. package/dist/config/validator.d.ts.map +1 -0
  178. package/dist/config/validator.js +112 -0
  179. package/dist/config/validator.js.map +1 -0
  180. package/dist/health/server.d.ts +47 -0
  181. package/dist/health/server.d.ts.map +1 -0
  182. package/dist/health/server.js +92 -0
  183. package/dist/health/server.js.map +1 -0
  184. package/dist/index.d.ts +21 -0
  185. package/dist/index.d.ts.map +1 -0
  186. package/dist/index.js +22 -0
  187. package/dist/index.js.map +1 -0
  188. package/dist/orchestrator/async-event-queue.d.ts +28 -0
  189. package/dist/orchestrator/async-event-queue.d.ts.map +1 -0
  190. package/dist/orchestrator/async-event-queue.js +57 -0
  191. package/dist/orchestrator/async-event-queue.js.map +1 -0
  192. package/dist/orchestrator/client.d.ts +110 -0
  193. package/dist/orchestrator/client.d.ts.map +1 -0
  194. package/dist/orchestrator/client.js +288 -0
  195. package/dist/orchestrator/client.js.map +1 -0
  196. package/dist/orchestrator/event-bus.d.ts +195 -0
  197. package/dist/orchestrator/event-bus.d.ts.map +1 -0
  198. package/dist/orchestrator/event-bus.js +557 -0
  199. package/dist/orchestrator/event-bus.js.map +1 -0
  200. package/dist/orchestrator/heartbeat.d.ts +71 -0
  201. package/dist/orchestrator/heartbeat.d.ts.map +1 -0
  202. package/dist/orchestrator/heartbeat.js +116 -0
  203. package/dist/orchestrator/heartbeat.js.map +1 -0
  204. package/dist/orchestrator/index.d.ts +25 -0
  205. package/dist/orchestrator/index.d.ts.map +1 -0
  206. package/dist/orchestrator/index.js +15 -0
  207. package/dist/orchestrator/index.js.map +1 -0
  208. package/dist/orchestrator/job-handler.d.ts +109 -0
  209. package/dist/orchestrator/job-handler.d.ts.map +1 -0
  210. package/dist/orchestrator/job-handler.js +612 -0
  211. package/dist/orchestrator/job-handler.js.map +1 -0
  212. package/dist/orchestrator/job-queue.d.ts +81 -0
  213. package/dist/orchestrator/job-queue.d.ts.map +1 -0
  214. package/dist/orchestrator/job-queue.js +206 -0
  215. package/dist/orchestrator/job-queue.js.map +1 -0
  216. package/dist/orchestrator/label-monitor-bridge.d.ts +25 -0
  217. package/dist/orchestrator/label-monitor-bridge.d.ts.map +1 -0
  218. package/dist/orchestrator/label-monitor-bridge.js +57 -0
  219. package/dist/orchestrator/label-monitor-bridge.js.map +1 -0
  220. package/dist/orchestrator/log-buffer.d.ts +74 -0
  221. package/dist/orchestrator/log-buffer.d.ts.map +1 -0
  222. package/dist/orchestrator/log-buffer.js +104 -0
  223. package/dist/orchestrator/log-buffer.js.map +1 -0
  224. package/dist/orchestrator/redis-job-queue.d.ts +44 -0
  225. package/dist/orchestrator/redis-job-queue.d.ts.map +1 -0
  226. package/dist/orchestrator/redis-job-queue.js +300 -0
  227. package/dist/orchestrator/redis-job-queue.js.map +1 -0
  228. package/dist/orchestrator/router.d.ts +125 -0
  229. package/dist/orchestrator/router.d.ts.map +1 -0
  230. package/dist/orchestrator/router.js +143 -0
  231. package/dist/orchestrator/router.js.map +1 -0
  232. package/dist/orchestrator/server.d.ts +62 -0
  233. package/dist/orchestrator/server.d.ts.map +1 -0
  234. package/dist/orchestrator/server.js +711 -0
  235. package/dist/orchestrator/server.js.map +1 -0
  236. package/dist/orchestrator/types.d.ts +184 -0
  237. package/dist/orchestrator/types.d.ts.map +1 -0
  238. package/dist/orchestrator/types.js +6 -0
  239. package/dist/orchestrator/types.js.map +1 -0
  240. package/dist/orchestrator/worker-registry.d.ts +110 -0
  241. package/dist/orchestrator/worker-registry.d.ts.map +1 -0
  242. package/dist/orchestrator/worker-registry.js +191 -0
  243. package/dist/orchestrator/worker-registry.js.map +1 -0
  244. package/package.json +80 -0
@@ -0,0 +1,711 @@
1
+ /**
2
+ * Orchestrator HTTP server.
3
+ * Provides REST API for worker registration, job distribution, and health monitoring.
4
+ */
5
+ import { createServer } from 'node:http';
6
+ import { randomUUID } from 'node:crypto';
7
+ import { WorkerRegistry } from './worker-registry.js';
8
+ import { InMemoryJobQueue } from './job-queue.js';
9
+ import { createRouter, pathToRegex, parseJsonBody, sendJson, sendError } from './router.js';
10
+ import { EventBus } from './event-bus.js';
11
+ import { LogBufferManager } from './log-buffer.js';
12
+ /**
13
+ * Default logger that writes to console
14
+ */
15
+ const defaultLogger = {
16
+ info: (message, data) => {
17
+ console.log(`[INFO] ${message}`, data ? JSON.stringify(data) : '');
18
+ },
19
+ warn: (message, data) => {
20
+ console.warn(`[WARN] ${message}`, data ? JSON.stringify(data) : '');
21
+ },
22
+ error: (message, data) => {
23
+ console.error(`[ERROR] ${message}`, data ? JSON.stringify(data) : '');
24
+ },
25
+ };
26
+ /**
27
+ * Create an orchestrator server
28
+ */
29
+ export function createOrchestratorServer(options = {}) {
30
+ const { port = 3100, host = '0.0.0.0', workerTimeout = 60000, authToken = process.env['ORCHESTRATOR_TOKEN'], eventBufferSize = 1000, eventGracePeriod = 300000, sseHeartbeatInterval = 30000, logger = defaultLogger, } = options;
31
+ // Initialize components
32
+ const workerRegistry = new WorkerRegistry({
33
+ heartbeatTimeout: workerTimeout,
34
+ onWorkerOffline: (workerId) => {
35
+ logger.warn('Worker went offline', { workerId });
36
+ },
37
+ onWorkerUnhealthy: (workerId) => {
38
+ logger.warn('Worker became unhealthy', { workerId });
39
+ },
40
+ });
41
+ const jobQueue = options.jobQueue ?? new InMemoryJobQueue();
42
+ const logBufferManager = new LogBufferManager({
43
+ gracePeriod: eventGracePeriod,
44
+ });
45
+ const eventBus = new EventBus({
46
+ bufferSize: eventBufferSize,
47
+ gracePeriod: eventGracePeriod,
48
+ heartbeatInterval: sseHeartbeatInterval,
49
+ jobQueue,
50
+ logBufferManager,
51
+ logger,
52
+ });
53
+ // Start periodic timeout check
54
+ let timeoutInterval = null;
55
+ /**
56
+ * Check authentication header
57
+ */
58
+ function authenticate(req) {
59
+ if (!authToken) {
60
+ return true; // Auth disabled
61
+ }
62
+ const authHeader = req.headers['authorization'];
63
+ if (!authHeader?.startsWith('Bearer ')) {
64
+ return false;
65
+ }
66
+ return authHeader.slice(7) === authToken;
67
+ }
68
+ /**
69
+ * Build routes using pathToRegex helper
70
+ */
71
+ const healthRoute = pathToRegex('/api/health');
72
+ const registerRoute = pathToRegex('/api/workers/register');
73
+ const unregisterRoute = pathToRegex('/api/workers/:workerId');
74
+ const heartbeatRoute = pathToRegex('/api/workers/:workerId/heartbeat');
75
+ const submitJobRoute = pathToRegex('/api/jobs');
76
+ const pollRoute = pathToRegex('/api/jobs/poll');
77
+ const getJobRoute = pathToRegex('/api/jobs/:jobId');
78
+ const statusRoute = pathToRegex('/api/jobs/:jobId/status');
79
+ const resultRoute = pathToRegex('/api/jobs/:jobId/result');
80
+ const cancelRoute = pathToRegex('/api/jobs/:jobId/cancel');
81
+ const jobEventsRoute = pathToRegex('/api/jobs/:jobId/events');
82
+ const jobLogsRoute = pathToRegex('/api/jobs/:jobId/logs');
83
+ const globalEventsRoute = pathToRegex('/api/events');
84
+ const router = createRouter([
85
+ { method: 'GET', pattern: healthRoute.regex, handler: 'healthCheck', paramNames: healthRoute.paramNames },
86
+ { method: 'POST', pattern: registerRoute.regex, handler: 'registerWorker', paramNames: registerRoute.paramNames },
87
+ { method: 'DELETE', pattern: unregisterRoute.regex, handler: 'unregisterWorker', paramNames: unregisterRoute.paramNames },
88
+ { method: 'POST', pattern: heartbeatRoute.regex, handler: 'handleHeartbeat', paramNames: heartbeatRoute.paramNames },
89
+ { method: 'POST', pattern: submitJobRoute.regex, handler: 'submitJob', paramNames: submitJobRoute.paramNames },
90
+ { method: 'GET', pattern: pollRoute.regex, handler: 'pollJob', paramNames: pollRoute.paramNames },
91
+ { method: 'GET', pattern: globalEventsRoute.regex, handler: 'subscribeAllEvents', paramNames: globalEventsRoute.paramNames },
92
+ { method: 'GET', pattern: jobLogsRoute.regex, handler: 'getJobLogs', paramNames: jobLogsRoute.paramNames },
93
+ { method: 'GET', pattern: jobEventsRoute.regex, handler: 'subscribeJobEvents', paramNames: jobEventsRoute.paramNames },
94
+ { method: 'POST', pattern: jobEventsRoute.regex, handler: 'publishEvent', paramNames: jobEventsRoute.paramNames },
95
+ { method: 'GET', pattern: getJobRoute.regex, handler: 'getJob', paramNames: getJobRoute.paramNames },
96
+ { method: 'PUT', pattern: statusRoute.regex, handler: 'updateJobStatus', paramNames: statusRoute.paramNames },
97
+ { method: 'POST', pattern: resultRoute.regex, handler: 'reportResult', paramNames: resultRoute.paramNames },
98
+ { method: 'POST', pattern: cancelRoute.regex, handler: 'cancelJob', paramNames: cancelRoute.paramNames },
99
+ ]);
100
+ /**
101
+ * Route handlers
102
+ */
103
+ const handlers = {
104
+ /**
105
+ * GET /api/health - Health check endpoint
106
+ */
107
+ async healthCheck(_req, res) {
108
+ const workerCounts = workerRegistry.getWorkerCounts();
109
+ sendJson(res, 200, {
110
+ status: 'healthy',
111
+ workers: workerCounts.total,
112
+ healthyWorkers: workerCounts.healthy,
113
+ unhealthyWorkers: workerCounts.unhealthy,
114
+ timestamp: new Date().toISOString(),
115
+ });
116
+ },
117
+ /**
118
+ * POST /api/jobs - Submit a new job
119
+ */
120
+ async submitJob(req, res) {
121
+ try {
122
+ const body = await parseJsonBody(req);
123
+ if (!body.name) {
124
+ sendError(res, 400, 'INVALID_REQUEST', 'Missing required field: name');
125
+ return;
126
+ }
127
+ if (!body.workflow) {
128
+ sendError(res, 400, 'INVALID_REQUEST', 'Missing required field: workflow');
129
+ return;
130
+ }
131
+ const job = {
132
+ ...body,
133
+ id: randomUUID(),
134
+ status: 'pending',
135
+ createdAt: new Date().toISOString(),
136
+ priority: body.priority ?? 'normal',
137
+ inputs: body.inputs ?? {},
138
+ };
139
+ await jobQueue.enqueue(job);
140
+ logger.info('Job submitted via API', { jobId: job.id, name: job.name });
141
+ sendJson(res, 201, { jobId: job.id, status: job.status });
142
+ }
143
+ catch (error) {
144
+ if (error instanceof Error && error.message === 'Invalid JSON') {
145
+ sendError(res, 400, 'INVALID_REQUEST', 'Invalid JSON in request body');
146
+ }
147
+ else {
148
+ logger.error('Error submitting job', { error: String(error) });
149
+ sendError(res, 500, 'INTERNAL_ERROR', 'Failed to submit job');
150
+ }
151
+ }
152
+ },
153
+ /**
154
+ * POST /api/workers/register - Register a worker
155
+ */
156
+ async registerWorker(req, res) {
157
+ try {
158
+ const body = await parseJsonBody(req);
159
+ if (!body.name) {
160
+ sendError(res, 400, 'INVALID_REQUEST', 'Missing required field: name');
161
+ return;
162
+ }
163
+ // Check if worker ID already exists
164
+ if (body.id && workerRegistry.getWorker(body.id)) {
165
+ sendError(res, 409, 'WORKER_ALREADY_EXISTS', `Worker with ID ${body.id} already exists`);
166
+ return;
167
+ }
168
+ const workerId = await workerRegistry.register(body);
169
+ logger.info('Worker registered', { workerId, name: body.name });
170
+ sendJson(res, 200, { workerId });
171
+ }
172
+ catch (error) {
173
+ if (error instanceof Error && error.message === 'Invalid JSON') {
174
+ sendError(res, 400, 'INVALID_REQUEST', 'Invalid JSON in request body');
175
+ }
176
+ else {
177
+ logger.error('Error registering worker', { error: String(error) });
178
+ sendError(res, 500, 'INTERNAL_ERROR', 'Failed to register worker');
179
+ }
180
+ }
181
+ },
182
+ /**
183
+ * DELETE /api/workers/:workerId - Unregister a worker
184
+ */
185
+ async unregisterWorker(_req, res, params) {
186
+ const { workerId } = params;
187
+ if (!workerId) {
188
+ sendError(res, 400, 'INVALID_REQUEST', 'Missing workerId parameter');
189
+ return;
190
+ }
191
+ const worker = workerRegistry.getWorker(workerId);
192
+ if (!worker) {
193
+ sendError(res, 404, 'WORKER_NOT_FOUND', `Worker with ID ${workerId} not found`);
194
+ return;
195
+ }
196
+ await workerRegistry.unregister(workerId);
197
+ logger.info('Worker unregistered', { workerId });
198
+ res.writeHead(204);
199
+ res.end();
200
+ },
201
+ /**
202
+ * POST /api/workers/:workerId/heartbeat - Handle heartbeat
203
+ */
204
+ async handleHeartbeat(req, res, params) {
205
+ const { workerId } = params;
206
+ if (!workerId) {
207
+ sendError(res, 400, 'INVALID_REQUEST', 'Missing workerId parameter');
208
+ return;
209
+ }
210
+ try {
211
+ const body = await parseJsonBody(req);
212
+ const worker = workerRegistry.getWorker(workerId);
213
+ if (!worker) {
214
+ sendError(res, 404, 'WORKER_NOT_FOUND', `Worker with ID ${workerId} not found`);
215
+ return;
216
+ }
217
+ const response = await workerRegistry.heartbeat(workerId, {
218
+ ...body,
219
+ workerId,
220
+ timestamp: new Date().toISOString(),
221
+ });
222
+ sendJson(res, 200, response);
223
+ }
224
+ catch (error) {
225
+ if (error instanceof Error && error.message === 'Invalid JSON') {
226
+ sendError(res, 400, 'INVALID_REQUEST', 'Invalid JSON in request body');
227
+ }
228
+ else {
229
+ logger.error('Error processing heartbeat', { error: String(error), workerId });
230
+ sendError(res, 500, 'INTERNAL_ERROR', 'Failed to process heartbeat');
231
+ }
232
+ }
233
+ },
234
+ /**
235
+ * GET /api/jobs/poll - Poll for available jobs
236
+ */
237
+ async pollJob(req, res) {
238
+ const url = new URL(req.url, `http://${req.headers.host}`);
239
+ const workerId = url.searchParams.get('workerId');
240
+ const capabilitiesParam = url.searchParams.get('capabilities');
241
+ if (!workerId) {
242
+ sendError(res, 400, 'INVALID_REQUEST', 'Missing required query parameter: workerId');
243
+ return;
244
+ }
245
+ const worker = workerRegistry.getWorker(workerId);
246
+ if (!worker) {
247
+ sendError(res, 404, 'WORKER_NOT_FOUND', `Worker with ID ${workerId} not found`);
248
+ return;
249
+ }
250
+ // Parse capabilities from comma-separated string
251
+ const capabilities = capabilitiesParam ? capabilitiesParam.split(',').map(c => c.trim()) : worker.capabilities;
252
+ // Pre-check: reject if worker is already at capacity
253
+ if (worker.currentJobs.length >= worker.maxConcurrent) {
254
+ sendJson(res, 200, { job: undefined, retryAfter: 5 });
255
+ return;
256
+ }
257
+ const job = await jobQueue.poll(workerId, capabilities);
258
+ if (job) {
259
+ const assigned = workerRegistry.assignJob(workerId, job.id);
260
+ if (!assigned) {
261
+ // Safety net: job was dequeued but worker can't accept it — put it back
262
+ logger.warn('Worker assignment failed after poll, requeuing job', {
263
+ jobId: job.id,
264
+ workerId,
265
+ });
266
+ await jobQueue.requeue(job.id);
267
+ sendJson(res, 200, { job: undefined, retryAfter: 5 });
268
+ return;
269
+ }
270
+ logger.info('Job assigned to worker', { jobId: job.id, workerId });
271
+ }
272
+ const response = {
273
+ job: job ?? undefined,
274
+ retryAfter: job ? undefined : 5,
275
+ };
276
+ sendJson(res, 200, response);
277
+ },
278
+ /**
279
+ * GET /api/jobs/:jobId - Get job details
280
+ */
281
+ async getJob(_req, res, params) {
282
+ const { jobId } = params;
283
+ if (!jobId) {
284
+ sendError(res, 400, 'INVALID_REQUEST', 'Missing jobId parameter');
285
+ return;
286
+ }
287
+ const job = await jobQueue.getJob(jobId);
288
+ if (!job) {
289
+ sendError(res, 404, 'JOB_NOT_FOUND', `Job with ID ${jobId} not found`);
290
+ return;
291
+ }
292
+ sendJson(res, 200, job);
293
+ },
294
+ /**
295
+ * GET /api/jobs/:jobId/events - Subscribe to SSE stream for a single job's events
296
+ */
297
+ async subscribeJobEvents(req, res, params) {
298
+ const { jobId } = params;
299
+ if (!jobId) {
300
+ sendError(res, 400, 'INVALID_REQUEST', 'Missing jobId parameter');
301
+ return;
302
+ }
303
+ const job = await jobQueue.getJob(jobId);
304
+ if (!job) {
305
+ sendError(res, 404, 'JOB_NOT_FOUND', `Job with ID ${jobId} not found`);
306
+ return;
307
+ }
308
+ const terminalStates = ['completed', 'failed', 'cancelled'];
309
+ // If job is in terminal state, replay buffered events and close
310
+ if (terminalStates.includes(job.status)) {
311
+ const bufferedEvents = eventBus.getBufferedEvents(jobId);
312
+ if (bufferedEvents.length === 0) {
313
+ sendError(res, 404, 'JOB_EVENTS_NOT_FOUND', `No buffered events for terminal job ${jobId}`);
314
+ return;
315
+ }
316
+ res.writeHead(200, {
317
+ 'Content-Type': 'text/event-stream',
318
+ 'Cache-Control': 'no-cache',
319
+ 'Connection': 'keep-alive',
320
+ 'X-Accel-Buffering': 'no',
321
+ });
322
+ res.flushHeaders();
323
+ for (const event of bufferedEvents) {
324
+ res.write(`event: ${event.type}\nid: ${event.id}\ndata: ${JSON.stringify(event)}\n\n`);
325
+ }
326
+ res.end();
327
+ return;
328
+ }
329
+ // Set SSE response headers for live stream
330
+ res.writeHead(200, {
331
+ 'Content-Type': 'text/event-stream',
332
+ 'Cache-Control': 'no-cache',
333
+ 'Connection': 'keep-alive',
334
+ 'X-Accel-Buffering': 'no',
335
+ });
336
+ res.flushHeaders();
337
+ // Parse Last-Event-ID for reconnection support
338
+ const lastEventId = req.headers['last-event-id'];
339
+ // Subscribe to the event bus (handles replay and live events)
340
+ eventBus.subscribe(jobId, res, lastEventId);
341
+ },
342
+ /**
343
+ * GET /api/jobs/:jobId/logs - Retrieve buffered log output
344
+ */
345
+ async getJobLogs(req, res, params) {
346
+ const { jobId } = params;
347
+ if (!jobId) {
348
+ sendError(res, 400, 'INVALID_REQUEST', 'Missing jobId parameter');
349
+ return;
350
+ }
351
+ const url = new URL(req.url, `http://${req.headers.host}`);
352
+ const sinceParam = url.searchParams.get('since');
353
+ const streamParam = url.searchParams.get('stream');
354
+ const logBuffer = logBufferManager.get(jobId);
355
+ // SSE streaming mode
356
+ if (streamParam === 'true') {
357
+ res.writeHead(200, {
358
+ 'Content-Type': 'text/event-stream',
359
+ 'Cache-Control': 'no-cache',
360
+ 'Connection': 'keep-alive',
361
+ 'X-Accel-Buffering': 'no',
362
+ });
363
+ res.flushHeaders();
364
+ // Send existing entries first
365
+ if (logBuffer) {
366
+ const entries = sinceParam
367
+ ? logBuffer.getAfterId(parseInt(sinceParam, 10))
368
+ : logBuffer.getAll();
369
+ for (const entry of entries) {
370
+ res.write(`event: log:append\nid: ${entry.id}\ndata: ${JSON.stringify(entry)}\n\n`);
371
+ }
372
+ }
373
+ // Subscribe to live log events via EventBus
374
+ eventBus.subscribe(jobId, res);
375
+ return;
376
+ }
377
+ // JSON mode: return buffered entries
378
+ if (!logBuffer) {
379
+ sendJson(res, 200, { entries: [], total: 0 });
380
+ return;
381
+ }
382
+ const entries = sinceParam
383
+ ? logBuffer.getAfterId(parseInt(sinceParam, 10))
384
+ : logBuffer.getAll();
385
+ sendJson(res, 200, { entries, total: logBuffer.size });
386
+ },
387
+ /**
388
+ * GET /api/events - Subscribe to SSE stream for all jobs (with optional filters)
389
+ */
390
+ async subscribeAllEvents(req, res) {
391
+ const url = new URL(req.url, `http://${req.headers.host}`);
392
+ // Parse filter query parameters
393
+ const tagsParam = url.searchParams.get('tags');
394
+ const workflowParam = url.searchParams.get('workflow');
395
+ const statusParam = url.searchParams.get('status');
396
+ const filters = {};
397
+ if (tagsParam) {
398
+ filters.tags = tagsParam.split(',').map((t) => t.trim()).filter(Boolean);
399
+ }
400
+ if (workflowParam) {
401
+ filters.workflow = workflowParam;
402
+ }
403
+ if (statusParam) {
404
+ filters.status = statusParam.split(',').map((s) => s.trim()).filter(Boolean);
405
+ }
406
+ // Set SSE response headers
407
+ res.writeHead(200, {
408
+ 'Content-Type': 'text/event-stream',
409
+ 'Cache-Control': 'no-cache',
410
+ 'Connection': 'keep-alive',
411
+ 'X-Accel-Buffering': 'no',
412
+ });
413
+ res.flushHeaders();
414
+ // Parse Last-Event-ID for reconnection support
415
+ const lastEventId = req.headers['last-event-id'];
416
+ // Subscribe to the event bus (handles replay and live events)
417
+ await eventBus.subscribeAll(res, filters, lastEventId);
418
+ },
419
+ /**
420
+ * POST /api/jobs/:jobId/events - Publish an event for a job
421
+ */
422
+ async publishEvent(req, res, params) {
423
+ const { jobId } = params;
424
+ if (!jobId) {
425
+ sendError(res, 400, 'INVALID_REQUEST', 'Missing jobId parameter');
426
+ return;
427
+ }
428
+ try {
429
+ const job = await jobQueue.getJob(jobId);
430
+ if (!job) {
431
+ sendError(res, 404, 'JOB_NOT_FOUND', `Job with ID ${jobId} not found`);
432
+ return;
433
+ }
434
+ const body = await parseJsonBody(req);
435
+ // Validate type field
436
+ const validEventTypes = [
437
+ 'job:status', 'phase:start', 'phase:complete',
438
+ 'step:start', 'step:complete', 'step:output',
439
+ 'action:error', 'log:append',
440
+ ];
441
+ if (!body.type || !validEventTypes.includes(body.type)) {
442
+ sendError(res, 400, 'INVALID_REQUEST', 'Missing or invalid field: type');
443
+ return;
444
+ }
445
+ // Validate data field
446
+ if (!body.data || typeof body.data !== 'object' || Array.isArray(body.data)) {
447
+ sendError(res, 400, 'INVALID_REQUEST', 'Missing or invalid field: data');
448
+ return;
449
+ }
450
+ const publishedEvent = await eventBus.publish(jobId, {
451
+ type: body.type,
452
+ timestamp: body.timestamp ?? Date.now(),
453
+ jobId,
454
+ data: body.data,
455
+ });
456
+ // Handle terminal status events
457
+ const terminalStatuses = ['completed', 'failed', 'cancelled'];
458
+ if (body.type === 'job:status' && terminalStatuses.includes(body.data.status)) {
459
+ eventBus.closeJobSubscribers(jobId);
460
+ eventBus.scheduleCleanup(jobId);
461
+ }
462
+ sendJson(res, 201, { eventId: publishedEvent.id });
463
+ }
464
+ catch (error) {
465
+ if (error instanceof Error && error.message === 'Invalid JSON') {
466
+ sendError(res, 400, 'INVALID_REQUEST', 'Invalid JSON in request body');
467
+ }
468
+ else {
469
+ logger.error('Error publishing event', { error: String(error), jobId });
470
+ sendError(res, 500, 'INTERNAL_ERROR', 'Failed to publish event');
471
+ }
472
+ }
473
+ },
474
+ /**
475
+ * PUT /api/jobs/:jobId/status - Update job status
476
+ */
477
+ async updateJobStatus(req, res, params) {
478
+ const { jobId } = params;
479
+ if (!jobId) {
480
+ sendError(res, 400, 'INVALID_REQUEST', 'Missing jobId parameter');
481
+ return;
482
+ }
483
+ try {
484
+ const body = await parseJsonBody(req);
485
+ if (!body.status) {
486
+ sendError(res, 400, 'INVALID_REQUEST', 'Missing required field: status');
487
+ return;
488
+ }
489
+ const job = await jobQueue.getJob(jobId);
490
+ if (!job) {
491
+ sendError(res, 404, 'JOB_NOT_FOUND', `Job with ID ${jobId} not found`);
492
+ return;
493
+ }
494
+ const previousStatus = job.status;
495
+ await jobQueue.updateStatus(jobId, body.status, body.metadata);
496
+ logger.info('Job status updated', { jobId, status: body.status });
497
+ // Auto-publish job:status event
498
+ await eventBus.publish(jobId, {
499
+ type: 'job:status',
500
+ timestamp: Date.now(),
501
+ jobId,
502
+ data: { status: body.status, previousStatus },
503
+ });
504
+ // Handle terminal status
505
+ const terminalStatuses = ['completed', 'failed', 'cancelled'];
506
+ if (terminalStatuses.includes(body.status)) {
507
+ eventBus.closeJobSubscribers(jobId);
508
+ eventBus.scheduleCleanup(jobId);
509
+ }
510
+ res.writeHead(204);
511
+ res.end();
512
+ }
513
+ catch (error) {
514
+ if (error instanceof Error && error.message === 'Invalid JSON') {
515
+ sendError(res, 400, 'INVALID_REQUEST', 'Invalid JSON in request body');
516
+ }
517
+ else {
518
+ logger.error('Error updating job status', { error: String(error), jobId });
519
+ sendError(res, 500, 'INTERNAL_ERROR', 'Failed to update job status');
520
+ }
521
+ }
522
+ },
523
+ /**
524
+ * POST /api/jobs/:jobId/result - Report job result
525
+ */
526
+ async reportResult(req, res, params) {
527
+ const { jobId } = params;
528
+ if (!jobId) {
529
+ sendError(res, 400, 'INVALID_REQUEST', 'Missing jobId parameter');
530
+ return;
531
+ }
532
+ try {
533
+ const body = await parseJsonBody(req);
534
+ if (!body.status) {
535
+ sendError(res, 400, 'INVALID_REQUEST', 'Missing required field: status');
536
+ return;
537
+ }
538
+ const job = await jobQueue.getJob(jobId);
539
+ if (!job) {
540
+ sendError(res, 404, 'JOB_NOT_FOUND', `Job with ID ${jobId} not found`);
541
+ return;
542
+ }
543
+ // Unassign job from worker
544
+ if (job.workerId) {
545
+ workerRegistry.unassignJob(job.workerId, jobId);
546
+ }
547
+ await jobQueue.reportResult(jobId, { ...body, jobId });
548
+ logger.info('Job result reported', { jobId, status: body.status });
549
+ res.writeHead(204);
550
+ res.end();
551
+ }
552
+ catch (error) {
553
+ if (error instanceof Error && error.message === 'Invalid JSON') {
554
+ sendError(res, 400, 'INVALID_REQUEST', 'Invalid JSON in request body');
555
+ }
556
+ else {
557
+ logger.error('Error reporting job result', { error: String(error), jobId });
558
+ sendError(res, 500, 'INTERNAL_ERROR', 'Failed to report job result');
559
+ }
560
+ }
561
+ },
562
+ /**
563
+ * POST /api/jobs/:jobId/cancel - Cancel a job
564
+ */
565
+ async cancelJob(req, res, params) {
566
+ const { jobId } = params;
567
+ if (!jobId) {
568
+ sendError(res, 400, 'INVALID_REQUEST', 'Missing jobId parameter');
569
+ return;
570
+ }
571
+ try {
572
+ const body = await parseJsonBody(req);
573
+ const job = await jobQueue.getJob(jobId);
574
+ if (!job) {
575
+ sendError(res, 404, 'JOB_NOT_FOUND', `Job with ID ${jobId} not found`);
576
+ return;
577
+ }
578
+ // Unassign job from worker if assigned
579
+ if (job.workerId) {
580
+ workerRegistry.unassignJob(job.workerId, jobId);
581
+ }
582
+ const previousStatus = job.status;
583
+ await jobQueue.cancelJob(jobId, body.reason);
584
+ logger.info('Job cancelled', { jobId, reason: body.reason });
585
+ // Auto-publish job:status event for cancellation
586
+ await eventBus.publish(jobId, {
587
+ type: 'job:status',
588
+ timestamp: Date.now(),
589
+ jobId,
590
+ data: { status: 'cancelled', previousStatus, reason: body.reason },
591
+ });
592
+ eventBus.closeJobSubscribers(jobId);
593
+ eventBus.scheduleCleanup(jobId);
594
+ res.writeHead(204);
595
+ res.end();
596
+ }
597
+ catch (error) {
598
+ if (error instanceof Error && error.message === 'Invalid JSON') {
599
+ sendError(res, 400, 'INVALID_REQUEST', 'Invalid JSON in request body');
600
+ }
601
+ else {
602
+ logger.error('Error cancelling job', { error: String(error), jobId });
603
+ sendError(res, 500, 'INTERNAL_ERROR', 'Failed to cancel job');
604
+ }
605
+ }
606
+ },
607
+ };
608
+ /**
609
+ * Create HTTP server
610
+ */
611
+ const server = createServer(async (req, res) => {
612
+ const url = new URL(req.url, `http://${req.headers.host}`);
613
+ const method = req.method ?? 'GET';
614
+ const path = url.pathname;
615
+ // Check authentication for non-health endpoints
616
+ if (path !== '/api/health' && !authenticate(req)) {
617
+ sendError(res, 401, 'UNAUTHORIZED', 'Authentication required');
618
+ return;
619
+ }
620
+ // Match route
621
+ const match = router(method, path);
622
+ if (!match) {
623
+ sendError(res, 404, 'NOT_FOUND', `Route not found: ${method} ${path}`);
624
+ return;
625
+ }
626
+ // Execute handler
627
+ const handler = handlers[match.handler];
628
+ if (!handler) {
629
+ sendError(res, 500, 'INTERNAL_ERROR', `Handler not found: ${match.handler}`);
630
+ return;
631
+ }
632
+ try {
633
+ await handler(req, res, match.params);
634
+ }
635
+ catch (error) {
636
+ logger.error('Unhandled error in request handler', { error: String(error), path, method });
637
+ sendError(res, 500, 'INTERNAL_ERROR', 'An unexpected error occurred');
638
+ }
639
+ });
640
+ let actualPort = port;
641
+ return {
642
+ async listen() {
643
+ return new Promise((resolve) => {
644
+ server.listen(port, host, () => {
645
+ const addr = server.address();
646
+ if (addr && typeof addr === 'object') {
647
+ actualPort = addr.port;
648
+ }
649
+ logger.info('Orchestrator server started', { host, port: actualPort });
650
+ // Start periodic worker timeout check
651
+ timeoutInterval = setInterval(async () => {
652
+ const offlineWorkers = await workerRegistry.checkTimeouts();
653
+ if (offlineWorkers.length > 0) {
654
+ logger.info('Workers went offline due to timeout', { workers: offlineWorkers });
655
+ }
656
+ }, workerTimeout / 2);
657
+ // Start SSE heartbeat
658
+ eventBus.startHeartbeat();
659
+ resolve();
660
+ });
661
+ });
662
+ },
663
+ async close() {
664
+ if (timeoutInterval) {
665
+ clearInterval(timeoutInterval);
666
+ timeoutInterval = null;
667
+ }
668
+ eventBus.destroy();
669
+ logBufferManager.destroy();
670
+ return new Promise((resolve, reject) => {
671
+ server.close((err) => {
672
+ if (err) {
673
+ reject(err);
674
+ }
675
+ else {
676
+ logger.info('Orchestrator server stopped');
677
+ resolve();
678
+ }
679
+ });
680
+ });
681
+ },
682
+ getPort() {
683
+ return actualPort;
684
+ },
685
+ async submitJob(jobData) {
686
+ const job = {
687
+ ...jobData,
688
+ id: randomUUID(),
689
+ status: 'pending',
690
+ createdAt: new Date().toISOString(),
691
+ priority: jobData.priority ?? 'normal',
692
+ };
693
+ await jobQueue.enqueue(job);
694
+ logger.info('Job submitted', { jobId: job.id, name: job.name });
695
+ return job.id;
696
+ },
697
+ getHttpServer() {
698
+ return server;
699
+ },
700
+ getWorkerRegistry() {
701
+ return workerRegistry;
702
+ },
703
+ getJobQueue() {
704
+ return jobQueue;
705
+ },
706
+ getEventBus() {
707
+ return eventBus;
708
+ },
709
+ };
710
+ }
711
+ //# sourceMappingURL=server.js.map