mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-10-05 11:17:50 +00:00
feat: add agent self-reporting protocol with registry, heartbeat, and discovery (closes #52)
- Agent Registry Service: registration, heartbeat, capability discovery, stale detection - REST API: POST /register, POST /:id/heartbeat, DELETE /:id, GET /stats, GET /capabilities/:cap - Persistent storage: .veritas-kanban/agent-registry.json - Auto-offline: agents without heartbeat for 5min marked offline - AGENTS.md template: docs/AGENTS-TEMPLATE.md with full integration guide
This commit is contained in:
parent
931d437b67
commit
fcf1756b79
4 changed files with 628 additions and 0 deletions
126
docs/AGENTS-TEMPLATE.md
Normal file
126
docs/AGENTS-TEMPLATE.md
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
# AGENTS.md Template — Veritas Kanban Self-Reporting Protocol
|
||||
|
||||
Use this template for agents that integrate with Veritas Kanban. Copy it into your agent's workspace and fill in the sections.
|
||||
|
||||
---
|
||||
|
||||
# AGENTS.md
|
||||
|
||||
## Identity
|
||||
|
||||
- **Agent ID:** `my-agent-id` _(unique, lowercase, dashes)_
|
||||
- **Name:** My Agent
|
||||
- **Model:** anthropic/claude-sonnet-4-5
|
||||
- **Provider:** anthropic
|
||||
- **Version:** 1.0.0
|
||||
|
||||
## Capabilities
|
||||
|
||||
List what this agent can do. Used for task routing.
|
||||
|
||||
- `code` — Write, review, and refactor code
|
||||
- `research` — Deep web research and analysis
|
||||
- `review` — Code review and PR feedback
|
||||
- `deploy` — CI/CD and deployment operations
|
||||
- `documentation` — Write and maintain docs
|
||||
|
||||
## Registration
|
||||
|
||||
On startup, register with Veritas Kanban:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3001/api/agents/register \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"id": "my-agent-id",
|
||||
"name": "My Agent",
|
||||
"model": "anthropic/claude-sonnet-4-5",
|
||||
"provider": "anthropic",
|
||||
"capabilities": [
|
||||
{"name": "code", "description": "Write and review code"},
|
||||
{"name": "research", "description": "Deep research and analysis"}
|
||||
],
|
||||
"version": "1.0.0"
|
||||
}'
|
||||
```
|
||||
|
||||
## Heartbeat
|
||||
|
||||
Send periodic heartbeats to stay registered (every 2-3 minutes):
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3001/api/agents/register/my-agent-id/heartbeat \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"status": "busy",
|
||||
"currentTaskId": "task_20260205_abc123",
|
||||
"currentTaskTitle": "Implement feature X"
|
||||
}'
|
||||
```
|
||||
|
||||
### Status Values
|
||||
|
||||
| Status | Meaning |
|
||||
|--------|---------|
|
||||
| `online` | Agent is available for work |
|
||||
| `busy` | Agent is actively working on a task |
|
||||
| `idle` | Agent is running but not doing anything |
|
||||
| `offline` | Agent hasn't sent a heartbeat in 5+ minutes (auto-set) |
|
||||
|
||||
## Deregistration
|
||||
|
||||
On shutdown, deregister cleanly:
|
||||
|
||||
```bash
|
||||
curl -X DELETE http://localhost:3001/api/agents/register/my-agent-id
|
||||
```
|
||||
|
||||
## Discovery
|
||||
|
||||
### List all agents
|
||||
```bash
|
||||
curl http://localhost:3001/api/agents/register
|
||||
```
|
||||
|
||||
### Filter by status
|
||||
```bash
|
||||
curl http://localhost:3001/api/agents/register?status=online
|
||||
```
|
||||
|
||||
### Filter by capability
|
||||
```bash
|
||||
curl http://localhost:3001/api/agents/register?capability=code
|
||||
```
|
||||
|
||||
### Find agents for a capability
|
||||
```bash
|
||||
curl http://localhost:3001/api/agents/register/capabilities/research
|
||||
```
|
||||
|
||||
### Registry stats
|
||||
```bash
|
||||
curl http://localhost:3001/api/agents/register/stats
|
||||
```
|
||||
|
||||
## Task Integration
|
||||
|
||||
When picking up a task:
|
||||
1. Send heartbeat with `status: "busy"` and `currentTaskId`
|
||||
2. Use existing task APIs: `POST /api/agents/:taskId/start`
|
||||
3. Report tokens: `POST /api/agents/:taskId/tokens`
|
||||
4. Complete: `POST /api/agents/:taskId/complete`
|
||||
5. Send heartbeat with `status: "idle"` and clear task
|
||||
|
||||
## Multi-Agent Coordination
|
||||
|
||||
The registry enables agents to discover each other:
|
||||
|
||||
```bash
|
||||
# Find who can help with code review
|
||||
curl http://localhost:3001/api/agents/register/capabilities/review
|
||||
|
||||
# Check if a specific agent is available
|
||||
curl http://localhost:3001/api/agents/register/codex-1
|
||||
```
|
||||
|
||||
This is the foundation for multi-agent task assignment (#29) and @mention notifications (#30).
|
||||
166
server/src/routes/agent-registry.ts
Normal file
166
server/src/routes/agent-registry.ts
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
/**
|
||||
* Agent Registry API Routes
|
||||
*
|
||||
* POST /api/agents/register — Register or update an agent
|
||||
* POST /api/agents/register/:id/heartbeat — Send heartbeat
|
||||
* DELETE /api/agents/register/:id — Deregister an agent
|
||||
* GET /api/agents/register — List all registered agents
|
||||
* GET /api/agents/register/:id — Get specific agent
|
||||
* GET /api/agents/register/stats — Get registry statistics
|
||||
* GET /api/agents/register/capabilities/:capability — Find agents by capability
|
||||
*/
|
||||
|
||||
import { Router, type Router as RouterType } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { getAgentRegistryService } from '../services/agent-registry-service.js';
|
||||
import { asyncHandler } from '../middleware/async-handler.js';
|
||||
import { NotFoundError, ValidationError } from '../middleware/error-handler.js';
|
||||
|
||||
const router: RouterType = Router();
|
||||
|
||||
// ─── Validation Schemas ──────────────────────────────────────────
|
||||
|
||||
const capabilitySchema = z.object({
|
||||
name: z.string().min(1).max(50),
|
||||
description: z.string().max(200).optional(),
|
||||
});
|
||||
|
||||
const registerSchema = z.object({
|
||||
id: z.string().min(1).max(50),
|
||||
name: z.string().min(1).max(100),
|
||||
model: z.string().max(100).optional(),
|
||||
provider: z.string().max(50).optional(),
|
||||
capabilities: z.array(capabilitySchema).optional(),
|
||||
version: z.string().max(50).optional(),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
sessionKey: z.string().max(200).optional(),
|
||||
});
|
||||
|
||||
const heartbeatSchema = z.object({
|
||||
status: z.enum(['online', 'busy', 'idle']).optional(),
|
||||
currentTaskId: z.string().max(100).optional().nullable(),
|
||||
currentTaskTitle: z.string().max(200).optional().nullable(),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
});
|
||||
|
||||
// ─── Routes ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* GET /api/agents/register/stats
|
||||
* Get registry statistics (must be before /:id to avoid collision)
|
||||
*/
|
||||
router.get(
|
||||
'/stats',
|
||||
asyncHandler(async (_req, res) => {
|
||||
const registry = getAgentRegistryService();
|
||||
res.json(registry.stats());
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /api/agents/register/capabilities/:capability
|
||||
* Find agents that have a specific capability
|
||||
*/
|
||||
router.get(
|
||||
'/capabilities/:capability',
|
||||
asyncHandler(async (req, res) => {
|
||||
const registry = getAgentRegistryService();
|
||||
const agents = registry.findByCapability(req.params.capability as string);
|
||||
res.json(agents);
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* POST /api/agents/register
|
||||
* Register or update an agent
|
||||
*/
|
||||
router.post(
|
||||
'/',
|
||||
asyncHandler(async (req, res) => {
|
||||
const parsed = registerSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
throw new ValidationError('Invalid registration', parsed.error.errors);
|
||||
}
|
||||
|
||||
const registry = getAgentRegistryService();
|
||||
const agent = registry.register(parsed.data);
|
||||
res.status(201).json(agent);
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /api/agents/register
|
||||
* List all registered agents, with optional filters
|
||||
*/
|
||||
router.get(
|
||||
'/',
|
||||
asyncHandler(async (req, res) => {
|
||||
const registry = getAgentRegistryService();
|
||||
const agents = registry.list({
|
||||
status: req.query.status as string | undefined,
|
||||
capability: req.query.capability as string | undefined,
|
||||
});
|
||||
res.json(agents);
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /api/agents/register/:id
|
||||
* Get a specific agent
|
||||
*/
|
||||
router.get(
|
||||
'/:id',
|
||||
asyncHandler(async (req, res) => {
|
||||
const registry = getAgentRegistryService();
|
||||
const agent = registry.get(req.params.id as string);
|
||||
if (!agent) {
|
||||
throw new NotFoundError('Agent not found');
|
||||
}
|
||||
res.json(agent);
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* POST /api/agents/register/:id/heartbeat
|
||||
* Send heartbeat for an agent
|
||||
*/
|
||||
router.post(
|
||||
'/:id/heartbeat',
|
||||
asyncHandler(async (req, res) => {
|
||||
const parsed = heartbeatSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
throw new ValidationError('Invalid heartbeat', parsed.error.errors);
|
||||
}
|
||||
|
||||
const registry = getAgentRegistryService();
|
||||
const agent = registry.heartbeat(req.params.id as string, {
|
||||
...parsed.data,
|
||||
currentTaskId: parsed.data.currentTaskId ?? undefined,
|
||||
currentTaskTitle: parsed.data.currentTaskTitle ?? undefined,
|
||||
});
|
||||
|
||||
if (!agent) {
|
||||
throw new NotFoundError('Agent not registered. Call POST /api/agents/register first.');
|
||||
}
|
||||
|
||||
res.json(agent);
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* DELETE /api/agents/register/:id
|
||||
* Deregister an agent
|
||||
*/
|
||||
router.delete(
|
||||
'/:id',
|
||||
asyncHandler(async (req, res) => {
|
||||
const registry = getAgentRegistryService();
|
||||
const removed = registry.deregister(req.params.id as string);
|
||||
if (!removed) {
|
||||
throw new NotFoundError('Agent not found');
|
||||
}
|
||||
res.json({ removed: true });
|
||||
})
|
||||
);
|
||||
|
||||
export { router as agentRegistryRoutes };
|
||||
|
|
@ -50,6 +50,7 @@ import { analyticsRoutes } from '../analytics.js';
|
|||
import tracesRoutes from '../traces.js';
|
||||
import { settingsRoutes } from '../settings.js';
|
||||
import { agentStatusRoutes } from '../agent-status.js';
|
||||
import { agentRegistryRoutes } from '../agent-registry.js';
|
||||
import { statusHistoryRoutes } from '../status-history.js';
|
||||
import digestRoutes from '../digest.js';
|
||||
import auditRoutes from '../audit.js';
|
||||
|
|
@ -118,6 +119,7 @@ v1Router.use('/traces', tracesRoutes);
|
|||
v1Router.use('/settings', settingsRoutes);
|
||||
v1Router.use('/settings/transition-hooks', transitionHooksRoutes);
|
||||
v1Router.use('/agent/status', agentStatusRoutes);
|
||||
v1Router.use('/agents/register', agentRegistryRoutes);
|
||||
v1Router.use('/status-history', statusHistoryRoutes);
|
||||
v1Router.use('/digest', digestRoutes);
|
||||
v1Router.use('/audit', auditRoutes);
|
||||
|
|
|
|||
334
server/src/services/agent-registry-service.ts
Normal file
334
server/src/services/agent-registry-service.ts
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
/**
|
||||
* Agent Registry Service
|
||||
*
|
||||
* Manages agent registration, heartbeat tracking, and capability discovery.
|
||||
* Agents register themselves with name, model, capabilities, and metadata.
|
||||
* The registry tracks liveness via heartbeats and exposes discovery APIs.
|
||||
*
|
||||
* Storage: File-based JSON in .veritas-kanban/agent-registry.json
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from '../storage/fs-helpers.js';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
|
||||
const log = createLogger('agent-registry');
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────
|
||||
|
||||
export interface AgentCapability {
|
||||
/** Capability name (e.g., "code", "research", "deploy", "review") */
|
||||
name: string;
|
||||
/** Optional description */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface RegisteredAgent {
|
||||
/** Unique agent identifier (e.g., "veritas", "codex-1", "sonnet-research") */
|
||||
id: string;
|
||||
/** Human-readable display name */
|
||||
name: string;
|
||||
/** Model identifier (e.g., "claude-opus-4-6", "gpt-5.2-codex") */
|
||||
model?: string;
|
||||
/** Provider (e.g., "anthropic", "openai-codex") */
|
||||
provider?: string;
|
||||
/** Agent capabilities */
|
||||
capabilities: AgentCapability[];
|
||||
/** Agent version or build info */
|
||||
version?: string;
|
||||
/** Freeform metadata */
|
||||
metadata?: Record<string, unknown>;
|
||||
/** Current status */
|
||||
status: 'online' | 'busy' | 'idle' | 'offline';
|
||||
/** ISO timestamp of registration */
|
||||
registeredAt: string;
|
||||
/** ISO timestamp of last heartbeat */
|
||||
lastHeartbeat: string;
|
||||
/** Current task ID (if working on something) */
|
||||
currentTaskId?: string;
|
||||
/** Current task title */
|
||||
currentTaskTitle?: string;
|
||||
/** Session key (for OpenClaw/orchestrator integration) */
|
||||
sessionKey?: string;
|
||||
}
|
||||
|
||||
export interface AgentRegistration {
|
||||
id: string;
|
||||
name: string;
|
||||
model?: string;
|
||||
provider?: string;
|
||||
capabilities?: AgentCapability[];
|
||||
version?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
sessionKey?: string;
|
||||
}
|
||||
|
||||
export interface AgentHeartbeat {
|
||||
status?: 'online' | 'busy' | 'idle';
|
||||
currentTaskId?: string;
|
||||
currentTaskTitle?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface AgentRegistryData {
|
||||
agents: Record<string, RegisteredAgent>;
|
||||
lastUpdated: string;
|
||||
}
|
||||
|
||||
// ─── Configuration ───────────────────────────────────────────────
|
||||
|
||||
/** How long before an agent is considered offline (no heartbeat) */
|
||||
const HEARTBEAT_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
/** How often to check for stale agents */
|
||||
const STALE_CHECK_INTERVAL_MS = 60 * 1000; // 1 minute
|
||||
|
||||
// ─── Service ─────────────────────────────────────────────────────
|
||||
|
||||
class AgentRegistryService {
|
||||
private agents: Map<string, RegisteredAgent> = new Map();
|
||||
private dataDir: string;
|
||||
private filePath: string;
|
||||
private staleCheckInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
constructor() {
|
||||
this.dataDir = process.env.VERITAS_DATA_DIR || path.join(process.cwd(), '..', '.veritas-kanban');
|
||||
this.filePath = path.join(this.dataDir, 'agent-registry.json');
|
||||
this.load();
|
||||
this.startStaleCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register or update an agent in the registry.
|
||||
*/
|
||||
register(registration: AgentRegistration): RegisteredAgent {
|
||||
const existing = this.agents.get(registration.id);
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const agent: RegisteredAgent = {
|
||||
id: registration.id,
|
||||
name: registration.name,
|
||||
model: registration.model ?? existing?.model,
|
||||
provider: registration.provider ?? existing?.provider,
|
||||
capabilities: registration.capabilities ?? existing?.capabilities ?? [],
|
||||
version: registration.version ?? existing?.version,
|
||||
metadata: registration.metadata ?? existing?.metadata,
|
||||
sessionKey: registration.sessionKey ?? existing?.sessionKey,
|
||||
status: 'online',
|
||||
registeredAt: existing?.registeredAt ?? now,
|
||||
lastHeartbeat: now,
|
||||
currentTaskId: existing?.currentTaskId,
|
||||
currentTaskTitle: existing?.currentTaskTitle,
|
||||
};
|
||||
|
||||
this.agents.set(registration.id, agent);
|
||||
this.persist();
|
||||
|
||||
log.info(
|
||||
{ agentId: agent.id, model: agent.model, capabilities: agent.capabilities.length },
|
||||
`Agent registered: ${agent.name}`
|
||||
);
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a heartbeat from an agent.
|
||||
*/
|
||||
heartbeat(agentId: string, update?: AgentHeartbeat): RegisteredAgent | null {
|
||||
const agent = this.agents.get(agentId);
|
||||
if (!agent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
agent.lastHeartbeat = new Date().toISOString();
|
||||
if (update?.status) agent.status = update.status;
|
||||
if (update?.currentTaskId !== undefined) agent.currentTaskId = update.currentTaskId || undefined;
|
||||
if (update?.currentTaskTitle !== undefined) agent.currentTaskTitle = update.currentTaskTitle || undefined;
|
||||
if (update?.metadata) agent.metadata = { ...agent.metadata, ...update.metadata };
|
||||
|
||||
this.agents.set(agentId, agent);
|
||||
this.persist();
|
||||
|
||||
return agent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deregister an agent.
|
||||
*/
|
||||
deregister(agentId: string): boolean {
|
||||
const existed = this.agents.delete(agentId);
|
||||
if (existed) {
|
||||
this.persist();
|
||||
log.info({ agentId }, `Agent deregistered: ${agentId}`);
|
||||
}
|
||||
return existed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific agent by ID.
|
||||
*/
|
||||
get(agentId: string): RegisteredAgent | null {
|
||||
return this.agents.get(agentId) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all registered agents, optionally filtered.
|
||||
*/
|
||||
list(filters?: {
|
||||
status?: string;
|
||||
capability?: string;
|
||||
}): RegisteredAgent[] {
|
||||
let agents = Array.from(this.agents.values());
|
||||
|
||||
if (filters?.status) {
|
||||
agents = agents.filter((a) => a.status === filters.status);
|
||||
}
|
||||
|
||||
if (filters?.capability) {
|
||||
const cap = filters.capability.toLowerCase();
|
||||
agents = agents.filter((a) =>
|
||||
a.capabilities.some((c) => c.name.toLowerCase() === cap)
|
||||
);
|
||||
}
|
||||
|
||||
return agents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find agents that have a specific capability.
|
||||
*/
|
||||
findByCapability(capability: string): RegisteredAgent[] {
|
||||
const cap = capability.toLowerCase();
|
||||
return Array.from(this.agents.values()).filter((a) =>
|
||||
a.status !== 'offline' && a.capabilities.some((c) => c.name.toLowerCase() === cap)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get registry statistics.
|
||||
*/
|
||||
stats(): {
|
||||
total: number;
|
||||
online: number;
|
||||
busy: number;
|
||||
idle: number;
|
||||
offline: number;
|
||||
capabilities: string[];
|
||||
} {
|
||||
const agents = Array.from(this.agents.values());
|
||||
const allCaps = new Set<string>();
|
||||
for (const agent of agents) {
|
||||
for (const cap of agent.capabilities) {
|
||||
allCaps.add(cap.name);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
total: agents.length,
|
||||
online: agents.filter((a) => a.status === 'online').length,
|
||||
busy: agents.filter((a) => a.status === 'busy').length,
|
||||
idle: agents.filter((a) => a.status === 'idle').length,
|
||||
offline: agents.filter((a) => a.status === 'offline').length,
|
||||
capabilities: Array.from(allCaps).sort(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark stale agents as offline.
|
||||
*/
|
||||
private checkStaleAgents(): void {
|
||||
const now = Date.now();
|
||||
let changed = false;
|
||||
|
||||
for (const agent of this.agents.values()) {
|
||||
if (agent.status === 'offline') continue;
|
||||
|
||||
const lastBeat = new Date(agent.lastHeartbeat).getTime();
|
||||
if (now - lastBeat > HEARTBEAT_TIMEOUT_MS) {
|
||||
agent.status = 'offline';
|
||||
changed = true;
|
||||
log.info(
|
||||
{ agentId: agent.id, lastHeartbeat: agent.lastHeartbeat },
|
||||
`Agent marked offline (heartbeat timeout): ${agent.id}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
this.persist();
|
||||
}
|
||||
}
|
||||
|
||||
private startStaleCheck(): void {
|
||||
this.staleCheckInterval = setInterval(() => this.checkStaleAgents(), STALE_CHECK_INTERVAL_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load registry from disk.
|
||||
*/
|
||||
private load(): void {
|
||||
try {
|
||||
if (existsSync(this.filePath)) {
|
||||
const raw = readFileSync(this.filePath, 'utf-8');
|
||||
const data = JSON.parse(raw) as AgentRegistryData;
|
||||
if (data.agents) {
|
||||
for (const [id, agent] of Object.entries(data.agents)) {
|
||||
this.agents.set(id, agent);
|
||||
}
|
||||
log.info({ count: this.agents.size }, 'Agent registry loaded from disk');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
log.warn({ err }, 'Could not load agent registry, starting fresh');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist registry to disk.
|
||||
*/
|
||||
private persist(): void {
|
||||
try {
|
||||
const dir = path.dirname(this.filePath);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
const data: AgentRegistryData = {
|
||||
agents: Object.fromEntries(this.agents),
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
|
||||
writeFileSync(this.filePath, JSON.stringify(data, null, 2), 'utf-8');
|
||||
} catch (err) {
|
||||
log.warn({ err }, 'Failed to persist agent registry');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources.
|
||||
*/
|
||||
dispose(): void {
|
||||
if (this.staleCheckInterval) {
|
||||
clearInterval(this.staleCheckInterval);
|
||||
this.staleCheckInterval = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton
|
||||
let instance: AgentRegistryService | null = null;
|
||||
|
||||
export function getAgentRegistryService(): AgentRegistryService {
|
||||
if (!instance) {
|
||||
instance = new AgentRegistryService();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
export function disposeAgentRegistryService(): void {
|
||||
if (instance) {
|
||||
instance.dispose();
|
||||
instance = null;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue