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

@@ -26,7 +26,7 @@ describe.skipIf(!process.env.INTEGRATION_TESTS)("Semantic Search Integration", (
// Initialize services
prisma = new PrismaClient();
const prismaService = prisma as unknown as PrismaService;
// Mock cache service for testing
cacheService = {
getSearch: async () => null,
@@ -37,11 +37,7 @@ describe.skipIf(!process.env.INTEGRATION_TESTS)("Semantic Search Integration", (
} as unknown as KnowledgeCacheService;
embeddingService = new EmbeddingService(prismaService);
searchService = new SearchService(
prismaService,
cacheService,
embeddingService
);
searchService = new SearchService(prismaService, cacheService, embeddingService);
// Create test workspace and user
const workspace = await prisma.workspace.create({
@@ -84,10 +80,7 @@ describe.skipIf(!process.env.INTEGRATION_TESTS)("Semantic Search Integration", (
const title = "Introduction to PostgreSQL";
const content = "PostgreSQL is a powerful open-source database.";
const prepared = embeddingService.prepareContentForEmbedding(
title,
content
);
const prepared = embeddingService.prepareContentForEmbedding(title, content);
// Title should appear twice for weighting
expect(prepared).toContain(title);
@@ -122,10 +115,7 @@ describe.skipIf(!process.env.INTEGRATION_TESTS)("Semantic Search Integration", (
it("should skip semantic search if OpenAI not configured", async () => {
if (!embeddingService.isConfigured()) {
await expect(
searchService.semanticSearch(
"database performance",
testWorkspaceId
)
searchService.semanticSearch("database performance", testWorkspaceId)
).rejects.toThrow();
} else {
// If configured, this is expected to work (tested below)
@@ -156,10 +146,7 @@ describe.skipIf(!process.env.INTEGRATION_TESTS)("Semantic Search Integration", (
entry.title,
entry.content
);
await embeddingService.generateAndStoreEmbedding(
created.id,
preparedContent
);
await embeddingService.generateAndStoreEmbedding(created.id, preparedContent);
}
// Wait a bit for embeddings to be stored
@@ -175,9 +162,7 @@ describe.skipIf(!process.env.INTEGRATION_TESTS)("Semantic Search Integration", (
expect(results.data.length).toBeGreaterThan(0);
// PostgreSQL entry should rank high for "relational database"
const postgresEntry = results.data.find(
(r) => r.slug === "postgresql-intro"
);
const postgresEntry = results.data.find((r) => r.slug === "postgresql-intro");
expect(postgresEntry).toBeDefined();
expect(postgresEntry!.rank).toBeGreaterThan(0);
},
@@ -187,18 +172,13 @@ describe.skipIf(!process.env.INTEGRATION_TESTS)("Semantic Search Integration", (
it.skipIf(!process.env["OPENAI_API_KEY"])(
"should perform hybrid search combining vector and keyword",
async () => {
const results = await searchService.hybridSearch(
"indexing",
testWorkspaceId
);
const results = await searchService.hybridSearch("indexing", testWorkspaceId);
// Should return results
expect(results.data.length).toBeGreaterThan(0);
// Should find the indexing entry
const indexingEntry = results.data.find(
(r) => r.slug === "database-indexing"
);
const indexingEntry = results.data.find((r) => r.slug === "database-indexing");
expect(indexingEntry).toBeDefined();
},
30000
@@ -230,15 +210,10 @@ describe.skipIf(!process.env.INTEGRATION_TESTS)("Semantic Search Integration", (
// Batch generate embeddings
const entriesForEmbedding = entries.map((e) => ({
id: e.id,
content: embeddingService.prepareContentForEmbedding(
e.title,
e.content
),
content: embeddingService.prepareContentForEmbedding(e.title, e.content),
}));
const successCount = await embeddingService.batchGenerateEmbeddings(
entriesForEmbedding
);
const successCount = await embeddingService.batchGenerateEmbeddings(entriesForEmbedding);
expect(successCount).toBe(3);