feat(orchestrator): MS23 agent lifecycle ingestion service (#701)
Some checks failed
ci/woodpecker/push/ci Pipeline failed

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #701.
This commit is contained in:
2026-03-07 04:21:26 +00:00
committed by jason.woltje
parent 49fa958444
commit de6faf659e
9 changed files with 262 additions and 20 deletions

View File

@@ -1,6 +1,7 @@
import { Injectable, Logger, Inject, forwardRef } from "@nestjs/common";
import { Injectable, Logger, Inject, Optional, forwardRef } from "@nestjs/common";
import { ValkeyService } from "../valkey/valkey.service";
import { AgentSpawnerService } from "./agent-spawner.service";
import { AgentIngestionService } from "../agent-ingestion/agent-ingestion.service";
import type { AgentState, AgentStatus, AgentEvent } from "../valkey/types";
import { isValidAgentTransition } from "../valkey/types/state.types";
@@ -32,7 +33,8 @@ export class AgentLifecycleService {
constructor(
private readonly valkeyService: ValkeyService,
@Inject(forwardRef(() => AgentSpawnerService))
private readonly spawnerService: AgentSpawnerService
private readonly spawnerService: AgentSpawnerService,
@Optional() private readonly agentIngestionService?: AgentIngestionService
) {
this.logger.log("AgentLifecycleService initialized");
}
@@ -55,6 +57,25 @@ export class AgentLifecycleService {
return createdState;
}
private async recordLifecycleIngestion(
agentId: string,
event: "started" | "completed" | "failed" | "killed",
record: (ingestionService: AgentIngestionService) => Promise<void>
): Promise<void> {
if (!this.agentIngestionService) {
return;
}
try {
await record(this.agentIngestionService);
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error(
`Failed to record agent ${event} ingestion for ${agentId}: ${errorMessage}`
);
}
}
/**
* Acquire a per-agent mutex to serialize state transitions.
* Uses promise chaining: each caller chains onto the previous lock,
@@ -118,6 +139,10 @@ export class AgentLifecycleService {
// Emit event
await this.publishStateChangeEvent("agent.running", updatedState);
await this.recordLifecycleIngestion(agentId, "started", (ingestionService) =>
ingestionService.recordAgentStarted(agentId)
);
this.logger.log(`Agent ${agentId} transitioned to running`);
return updatedState;
});
@@ -155,6 +180,10 @@ export class AgentLifecycleService {
// Emit event
await this.publishStateChangeEvent("agent.completed", updatedState);
await this.recordLifecycleIngestion(agentId, "completed", (ingestionService) =>
ingestionService.recordAgentCompleted(agentId)
);
// Schedule session cleanup
this.spawnerService.scheduleSessionCleanup(agentId);
@@ -192,6 +221,10 @@ export class AgentLifecycleService {
// Emit event
await this.publishStateChangeEvent("agent.failed", updatedState, error);
await this.recordLifecycleIngestion(agentId, "failed", (ingestionService) =>
ingestionService.recordAgentFailed(agentId, error)
);
// Schedule session cleanup
this.spawnerService.scheduleSessionCleanup(agentId);
@@ -228,6 +261,10 @@ export class AgentLifecycleService {
// Emit event
await this.publishStateChangeEvent("agent.killed", updatedState);
await this.recordLifecycleIngestion(agentId, "killed", (ingestionService) =>
ingestionService.recordAgentKilled(agentId)
);
// Schedule session cleanup
this.spawnerService.scheduleSessionCleanup(agentId);

View File

@@ -1,4 +1,11 @@
import { Injectable, Logger, HttpException, HttpStatus, OnModuleDestroy } from "@nestjs/common";
import {
Injectable,
Logger,
HttpException,
HttpStatus,
OnModuleDestroy,
Optional,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import Anthropic from "@anthropic-ai/sdk";
import { randomUUID } from "crypto";
@@ -8,6 +15,7 @@ import {
AgentSession,
AgentType,
} from "./types/agent-spawner.types";
import { AgentIngestionService } from "../agent-ingestion/agent-ingestion.service";
/**
* Default delay in milliseconds before cleaning up sessions after terminal states
@@ -30,7 +38,10 @@ export class AgentSpawnerService implements OnModuleDestroy {
private readonly sessionCleanupDelayMs: number;
private readonly cleanupTimers = new Map<string, NodeJS.Timeout>();
constructor(private readonly configService: ConfigService) {
constructor(
private readonly configService: ConfigService,
@Optional() private readonly agentIngestionService?: AgentIngestionService
) {
const configuredProvider = this.configService.get<string>("orchestrator.aiProvider");
this.aiProvider = this.normalizeAiProvider(configuredProvider);
@@ -98,6 +109,19 @@ export class AgentSpawnerService implements OnModuleDestroy {
this.cleanupTimers.clear();
}
private recordSpawnedAgentIngestion(agentId: string, request: SpawnAgentRequest): void {
if (!this.agentIngestionService) {
return;
}
void this.agentIngestionService
.recordAgentSpawned(agentId, undefined, undefined, request.taskId, request.agentType)
.catch((error: unknown) => {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error(`Failed to record spawned ingestion for ${agentId}: ${errorMessage}`);
});
}
/**
* Spawn a new agent with the given configuration
* @param request Agent spawn request
@@ -130,6 +154,8 @@ export class AgentSpawnerService implements OnModuleDestroy {
// Store session
this.sessions.set(agentId, session);
this.recordSpawnedAgentIngestion(agentId, request);
this.logger.log(`Agent spawned successfully: ${agentId} (type: ${request.agentType})`);
// NOTE: Actual Claude SDK integration will be implemented in next iteration (see issue #TBD)

View File

@@ -3,9 +3,10 @@ import { AgentSpawnerService } from "./agent-spawner.service";
import { AgentLifecycleService } from "./agent-lifecycle.service";
import { DockerSandboxService } from "./docker-sandbox.service";
import { ValkeyModule } from "../valkey/valkey.module";
import { AgentIngestionModule } from "../agent-ingestion/agent-ingestion.module";
@Module({
imports: [ValkeyModule],
imports: [ValkeyModule, AgentIngestionModule],
providers: [AgentSpawnerService, AgentLifecycleService, DockerSandboxService],
exports: [AgentSpawnerService, AgentLifecycleService, DockerSandboxService],
})