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:
@@ -1,9 +1,11 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Get,
|
||||
Body,
|
||||
Param,
|
||||
BadRequestException,
|
||||
NotFoundException,
|
||||
Logger,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
@@ -11,6 +13,7 @@ import {
|
||||
} from "@nestjs/common";
|
||||
import { QueueService } from "../../queue/queue.service";
|
||||
import { AgentSpawnerService } from "../../spawner/agent-spawner.service";
|
||||
import { AgentLifecycleService } from "../../spawner/agent-lifecycle.service";
|
||||
import { KillswitchService } from "../../killswitch/killswitch.service";
|
||||
import { SpawnAgentDto, SpawnAgentResponseDto } from "./dto/spawn-agent.dto";
|
||||
|
||||
@@ -24,6 +27,7 @@ export class AgentsController {
|
||||
constructor(
|
||||
private readonly queueService: QueueService,
|
||||
private readonly spawnerService: AgentSpawnerService,
|
||||
private readonly lifecycleService: AgentLifecycleService,
|
||||
private readonly killswitchService: KillswitchService
|
||||
) {}
|
||||
|
||||
@@ -66,6 +70,64 @@ export class AgentsController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent status
|
||||
* @param agentId Agent ID to query
|
||||
* @returns Agent status details
|
||||
*/
|
||||
@Get(":agentId/status")
|
||||
async getAgentStatus(@Param("agentId") agentId: string): Promise<{
|
||||
agentId: string;
|
||||
taskId: string;
|
||||
status: string;
|
||||
spawnedAt: string;
|
||||
startedAt?: string;
|
||||
completedAt?: string;
|
||||
error?: string;
|
||||
}> {
|
||||
this.logger.log(`Received status request for agent: ${agentId}`);
|
||||
|
||||
try {
|
||||
// Try to get from lifecycle service (Valkey)
|
||||
const lifecycleState = await this.lifecycleService.getAgentLifecycleState(agentId);
|
||||
|
||||
if (lifecycleState) {
|
||||
return {
|
||||
agentId: lifecycleState.agentId,
|
||||
taskId: lifecycleState.taskId,
|
||||
status: lifecycleState.status,
|
||||
spawnedAt: lifecycleState.startedAt ?? new Date().toISOString(),
|
||||
startedAt: lifecycleState.startedAt,
|
||||
completedAt: lifecycleState.completedAt,
|
||||
error: lifecycleState.error,
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback to spawner service (in-memory)
|
||||
const session = this.spawnerService.getAgentSession(agentId);
|
||||
|
||||
if (session) {
|
||||
return {
|
||||
agentId: session.agentId,
|
||||
taskId: session.taskId,
|
||||
status: session.state,
|
||||
spawnedAt: session.spawnedAt.toISOString(),
|
||||
completedAt: session.completedAt?.toISOString(),
|
||||
error: session.error,
|
||||
};
|
||||
}
|
||||
|
||||
throw new NotFoundException(`Agent ${agentId} not found`);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
this.logger.error(`Failed to get agent status: ${errorMessage}`);
|
||||
throw new Error(`Failed to get agent status: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill a single agent immediately
|
||||
* @param agentId Agent ID to kill
|
||||
|
||||
@@ -3,9 +3,10 @@ import { AgentsController } from "./agents.controller";
|
||||
import { QueueModule } from "../../queue/queue.module";
|
||||
import { SpawnerModule } from "../../spawner/spawner.module";
|
||||
import { KillswitchModule } from "../../killswitch/killswitch.module";
|
||||
import { ValkeyModule } from "../../valkey/valkey.module";
|
||||
|
||||
@Module({
|
||||
imports: [QueueModule, SpawnerModule, KillswitchModule],
|
||||
imports: [QueueModule, SpawnerModule, KillswitchModule, ValkeyModule],
|
||||
controllers: [AgentsController],
|
||||
})
|
||||
export class AgentsModule {}
|
||||
|
||||
Reference in New Issue
Block a user