feat(#93): implement agent spawn via federation

Implements FED-010: Agent Spawn via Federation feature that enables
spawning and managing Claude agents on remote federated Mosaic Stack
instances via COMMAND message type.

Features:
- Federation agent command types (spawn, status, kill)
- FederationAgentService for handling agent operations
- Integration with orchestrator's agent spawner/lifecycle services
- API endpoints for spawning, querying status, and killing agents
- Full command routing through federation COMMAND infrastructure
- Comprehensive test coverage (12/12 tests passing)

Architecture:
- Hub → Spoke: Spawn agents on remote instances
- Command flow: FederationController → FederationAgentService →
  CommandService → Remote Orchestrator
- Response handling: Remote orchestrator returns agent status/results
- Security: Connection validation, signature verification

Files created:
- apps/api/src/federation/types/federation-agent.types.ts
- apps/api/src/federation/federation-agent.service.ts
- apps/api/src/federation/federation-agent.service.spec.ts

Files modified:
- apps/api/src/federation/command.service.ts (agent command routing)
- apps/api/src/federation/federation.controller.ts (agent endpoints)
- apps/api/src/federation/federation.module.ts (service registration)
- apps/orchestrator/src/api/agents/agents.controller.ts (status endpoint)
- apps/orchestrator/src/api/agents/agents.module.ts (lifecycle integration)

Testing:
- 12/12 tests passing for FederationAgentService
- All command service tests passing
- TypeScript compilation successful
- Linting passed

Refs #93

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-03 14:37:06 -06:00
parent a8c8af21e5
commit 12abdfe81d
405 changed files with 13545 additions and 2153 deletions

View File

@@ -69,8 +69,8 @@ docker compose up -d valkey
### 1. Inject the Service
```typescript
import { Injectable } from '@nestjs/common';
import { ValkeyService } from './valkey/valkey.service';
import { Injectable } from "@nestjs/common";
import { ValkeyService } from "./valkey/valkey.service";
@Injectable()
export class MyService {
@@ -82,11 +82,11 @@ export class MyService {
```typescript
const task = await this.valkeyService.enqueue({
type: 'send-email',
type: "send-email",
data: {
to: 'user@example.com',
subject: 'Welcome!',
body: 'Hello, welcome to Mosaic Stack',
to: "user@example.com",
subject: "Welcome!",
body: "Hello, welcome to Mosaic Stack",
},
});
@@ -102,11 +102,11 @@ const task = await this.valkeyService.dequeue();
if (task) {
console.log(task.status); // 'processing'
try {
// Do work...
await sendEmail(task.data);
// Mark as completed
await this.valkeyService.updateStatus(task.id, {
status: TaskStatus.COMPLETED,
@@ -129,8 +129,8 @@ const status = await this.valkeyService.getStatus(taskId);
if (status) {
console.log(status.status); // 'completed' | 'failed' | 'processing' | 'pending'
console.log(status.data); // Task metadata
console.log(status.error); // Error message if failed
console.log(status.data); // Task metadata
console.log(status.error); // Error message if failed
}
```
@@ -143,7 +143,7 @@ console.log(`${length} tasks in queue`);
// Health check
const healthy = await this.valkeyService.healthCheck();
console.log(`Valkey is ${healthy ? 'healthy' : 'down'}`);
console.log(`Valkey is ${healthy ? "healthy" : "down"}`);
// Clear queue (use with caution!)
await this.valkeyService.clearQueue();
@@ -181,12 +181,12 @@ export class EmailWorker {
private async startWorker() {
while (true) {
const task = await this.valkeyService.dequeue();
if (task) {
await this.processTask(task);
} else {
// No tasks, wait 5 seconds
await new Promise(resolve => setTimeout(resolve, 5000));
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
}
@@ -194,14 +194,14 @@ export class EmailWorker {
private async processTask(task: TaskDto) {
try {
switch (task.type) {
case 'send-email':
case "send-email":
await this.sendEmail(task.data);
break;
case 'generate-report':
case "generate-report":
await this.generateReport(task.data);
break;
}
await this.valkeyService.updateStatus(task.id, {
status: TaskStatus.COMPLETED,
});
@@ -222,10 +222,10 @@ export class EmailWorker {
export class ScheduledTasks {
constructor(private readonly valkeyService: ValkeyService) {}
@Cron('0 0 * * *') // Daily at midnight
@Cron("0 0 * * *") // Daily at midnight
async dailyReport() {
await this.valkeyService.enqueue({
type: 'daily-report',
type: "daily-report",
data: { date: new Date().toISOString() },
});
}
@@ -241,6 +241,7 @@ pnpm test valkey.service.spec.ts
```
Tests cover:
- ✅ Connection and initialization
- ✅ Enqueue operations
- ✅ Dequeue FIFO behavior
@@ -254,9 +255,11 @@ Tests cover:
### ValkeyService Methods
#### `enqueue(task: EnqueueTaskDto): Promise<TaskDto>`
Add a task to the queue.
**Parameters:**
- `task.type` (string): Task type identifier
- `task.data` (object): Task metadata
@@ -265,6 +268,7 @@ Add a task to the queue.
---
#### `dequeue(): Promise<TaskDto | null>`
Get the next task from the queue (FIFO).
**Returns:** Next task with status updated to PROCESSING, or null if queue is empty
@@ -272,9 +276,11 @@ Get the next task from the queue (FIFO).
---
#### `getStatus(taskId: string): Promise<TaskDto | null>`
Retrieve task status and metadata.
**Parameters:**
- `taskId` (string): Task UUID
**Returns:** Task data or null if not found
@@ -282,9 +288,11 @@ Retrieve task status and metadata.
---
#### `updateStatus(taskId: string, update: UpdateTaskStatusDto): Promise<TaskDto | null>`
Update task status and optionally add results or errors.
**Parameters:**
- `taskId` (string): Task UUID
- `update.status` (TaskStatus): New status
- `update.error` (string, optional): Error message for failed tasks
@@ -295,6 +303,7 @@ Update task status and optionally add results or errors.
---
#### `getQueueLength(): Promise<number>`
Get the number of tasks in queue.
**Returns:** Queue length
@@ -302,11 +311,13 @@ Get the number of tasks in queue.
---
#### `clearQueue(): Promise<void>`
Remove all tasks from queue (metadata remains until TTL).
---
#### `healthCheck(): Promise<boolean>`
Verify Valkey connectivity.
**Returns:** true if connected, false otherwise
@@ -314,6 +325,7 @@ Verify Valkey connectivity.
## Migration Notes
If upgrading from BullMQ or another queue system:
1. Task IDs are UUIDs (not incremental)
2. No built-in retry mechanism (implement in worker)
3. No job priorities (strict FIFO)
@@ -329,7 +341,7 @@ For advanced features like retries, priorities, or scheduled jobs, consider wrap
// Check Valkey connectivity
const healthy = await this.valkeyService.healthCheck();
if (!healthy) {
console.error('Valkey is not responding');
console.error("Valkey is not responding");
}
```
@@ -349,6 +361,7 @@ docker exec -it mosaic-valkey valkey-cli DEL mosaic:task:queue
### Debug Logging
The service logs all operations at `info` level. Check application logs for:
- Task enqueue/dequeue operations
- Status updates
- Connection events
@@ -356,6 +369,7 @@ The service logs all operations at `info` level. Check application logs for:
## Future Enhancements
Potential improvements for consideration:
- [ ] Task priorities (weighted queues)
- [ ] Retry mechanism with exponential backoff
- [ ] Delayed/scheduled tasks