feat(orchestrator): add SSE events, queue controls, and mosaic rails sync
This commit is contained in:
@@ -2,9 +2,10 @@ import { Module } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { QueueService } from "./queue.service";
|
||||
import { ValkeyModule } from "../valkey/valkey.module";
|
||||
import { SpawnerModule } from "../spawner/spawner.module";
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule, ValkeyModule],
|
||||
imports: [ConfigModule, ValkeyModule, SpawnerModule],
|
||||
providers: [QueueService],
|
||||
exports: [QueueService],
|
||||
})
|
||||
|
||||
@@ -991,12 +991,17 @@ describe("QueueService", () => {
|
||||
success: true,
|
||||
metadata: { attempt: 1 },
|
||||
});
|
||||
expect(mockValkeyService.updateTaskStatus).toHaveBeenCalledWith("task-123", "executing");
|
||||
expect(mockValkeyService.updateTaskStatus).toHaveBeenCalledWith(
|
||||
"task-123",
|
||||
"executing",
|
||||
undefined
|
||||
);
|
||||
expect(mockValkeyService.publishEvent).toHaveBeenCalledWith({
|
||||
type: "task.processing",
|
||||
type: "task.executing",
|
||||
timestamp: expect.any(String),
|
||||
taskId: "task-123",
|
||||
data: { attempt: 1 },
|
||||
agentId: undefined,
|
||||
data: { attempt: 1, dispatchedByQueue: true },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Injectable, OnModuleDestroy, OnModuleInit } from "@nestjs/common";
|
||||
import { Injectable, OnModuleDestroy, OnModuleInit, Optional, Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { Queue, Worker, Job } from "bullmq";
|
||||
import { ValkeyService } from "../valkey/valkey.service";
|
||||
import { AgentSpawnerService } from "../spawner/agent-spawner.service";
|
||||
import { AgentLifecycleService } from "../spawner/agent-lifecycle.service";
|
||||
import type { TaskContext } from "../valkey/types";
|
||||
import type {
|
||||
QueuedTask,
|
||||
@@ -16,6 +18,7 @@ import type {
|
||||
*/
|
||||
@Injectable()
|
||||
export class QueueService implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly logger = new Logger(QueueService.name);
|
||||
private queue!: Queue<QueuedTask>;
|
||||
private worker!: Worker<QueuedTask, TaskProcessingResult>;
|
||||
private readonly queueName: string;
|
||||
@@ -23,7 +26,9 @@ export class QueueService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
constructor(
|
||||
private readonly valkeyService: ValkeyService,
|
||||
private readonly configService: ConfigService
|
||||
private readonly configService: ConfigService,
|
||||
@Optional() private readonly spawnerService?: AgentSpawnerService,
|
||||
@Optional() private readonly lifecycleService?: AgentLifecycleService
|
||||
) {
|
||||
this.queueName = this.configService.get<string>(
|
||||
"orchestrator.queue.name",
|
||||
@@ -132,6 +137,16 @@ export class QueueService implements OnModuleInit, OnModuleDestroy {
|
||||
context,
|
||||
};
|
||||
|
||||
// Ensure task state exists before queue lifecycle updates.
|
||||
const getTaskState = (this.valkeyService as Partial<ValkeyService>).getTaskState;
|
||||
const createTask = (this.valkeyService as Partial<ValkeyService>).createTask;
|
||||
if (typeof getTaskState === "function" && typeof createTask === "function") {
|
||||
const existingTask = await getTaskState.call(this.valkeyService, taskId);
|
||||
if (!existingTask) {
|
||||
await createTask.call(this.valkeyService, taskId, context);
|
||||
}
|
||||
}
|
||||
|
||||
// Add to BullMQ queue
|
||||
await this.queue.add(taskId, queuedTask, {
|
||||
priority: 10 - priority + 1, // BullMQ: lower number = higher priority, so invert
|
||||
@@ -214,23 +229,35 @@ export class QueueService implements OnModuleInit, OnModuleDestroy {
|
||||
const { taskId } = job.data;
|
||||
|
||||
try {
|
||||
const session = this.spawnerService?.findAgentSessionByTaskId(taskId);
|
||||
const agentId = session?.agentId;
|
||||
|
||||
if (agentId) {
|
||||
if (this.lifecycleService) {
|
||||
await this.lifecycleService.transitionToRunning(agentId);
|
||||
}
|
||||
this.spawnerService?.setSessionState(agentId, "running");
|
||||
}
|
||||
|
||||
// Update task state to executing
|
||||
await this.valkeyService.updateTaskStatus(taskId, "executing");
|
||||
await this.valkeyService.updateTaskStatus(taskId, "executing", agentId);
|
||||
|
||||
// Publish event
|
||||
await this.valkeyService.publishEvent({
|
||||
type: "task.processing",
|
||||
type: "task.executing",
|
||||
timestamp: new Date().toISOString(),
|
||||
taskId,
|
||||
data: { attempt: job.attemptsMade + 1 },
|
||||
agentId,
|
||||
data: {
|
||||
attempt: job.attemptsMade + 1,
|
||||
dispatchedByQueue: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Task processing will be handled by agent spawner
|
||||
// For now, just mark as processing
|
||||
return {
|
||||
success: true,
|
||||
metadata: {
|
||||
attempt: job.attemptsMade + 1,
|
||||
...(agentId && { agentId }),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -270,6 +297,14 @@ export class QueueService implements OnModuleInit, OnModuleDestroy {
|
||||
* Handle task failure
|
||||
*/
|
||||
private async handleTaskFailure(taskId: string, error: Error): Promise<void> {
|
||||
const session = this.spawnerService?.findAgentSessionByTaskId(taskId);
|
||||
if (session) {
|
||||
this.spawnerService?.setSessionState(session.agentId, "failed", error.message, new Date());
|
||||
if (this.lifecycleService) {
|
||||
await this.lifecycleService.transitionToFailed(session.agentId, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
await this.valkeyService.updateTaskStatus(taskId, "failed", undefined, error.message);
|
||||
|
||||
await this.valkeyService.publishEvent({
|
||||
@@ -284,12 +319,25 @@ export class QueueService implements OnModuleInit, OnModuleDestroy {
|
||||
* Handle task completion
|
||||
*/
|
||||
private async handleTaskCompletion(taskId: string): Promise<void> {
|
||||
const session = this.spawnerService?.findAgentSessionByTaskId(taskId);
|
||||
if (session) {
|
||||
this.spawnerService?.setSessionState(session.agentId, "completed", undefined, new Date());
|
||||
if (this.lifecycleService) {
|
||||
await this.lifecycleService.transitionToCompleted(session.agentId);
|
||||
}
|
||||
} else {
|
||||
this.logger.warn(
|
||||
`Queue completed task ${taskId} but no session was found; using queue-only completion state`
|
||||
);
|
||||
}
|
||||
|
||||
await this.valkeyService.updateTaskStatus(taskId, "completed");
|
||||
|
||||
await this.valkeyService.publishEvent({
|
||||
type: "task.completed",
|
||||
timestamp: new Date().toISOString(),
|
||||
taskId,
|
||||
...(session && { agentId: session.agentId }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user