From 90b0094aae9b666ca714571fe7ec02e9eb457f2c Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 14 Nov 2025 20:20:17 +0100 Subject: [PATCH] feat: add agent registry --- .../server/src/server/agent/agent-registry.ts | 205 ++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 packages/server/src/server/agent/agent-registry.ts diff --git a/packages/server/src/server/agent/agent-registry.ts b/packages/server/src/server/agent/agent-registry.ts new file mode 100644 index 000000000..7463d5cd3 --- /dev/null +++ b/packages/server/src/server/agent/agent-registry.ts @@ -0,0 +1,205 @@ +import { promises as fs } from "node:fs"; +import path from "node:path"; +import { z } from "zod"; + +import type { AgentSnapshot } from "./agent-manager.js"; +import type { + AgentPersistenceHandle, + AgentProvider, + AgentSessionConfig, +} from "./agent-sdk-types.js"; + +const SERIALIZABLE_CONFIG_SCHEMA = z + .object({ + modeId: z.string().nullable().optional(), + model: z.string().nullable().optional(), + extra: z.record(z.any()).nullable().optional(), + }) + .nullable() + .optional(); + +const PERSISTENCE_HANDLE_SCHEMA = z + .object({ + provider: z.string(), + sessionId: z.string(), + nativeHandle: z.any().optional(), + metadata: z.record(z.any()).optional(), + }) + .nullable() + .optional(); + +const STORED_AGENT_SCHEMA = z.object({ + id: z.string(), + provider: z.string(), + cwd: z.string(), + createdAt: z.string(), + updatedAt: z.string(), + title: z.string().nullable().optional(), + lastStatus: z.string().nullable().optional(), + lastModeId: z.string().nullable().optional(), + config: SERIALIZABLE_CONFIG_SCHEMA, + persistence: PERSISTENCE_HANDLE_SCHEMA, +}); + +export type SerializableAgentConfig = Pick< + AgentSessionConfig, + "modeId" | "model" | "extra" +>; + +export type StoredAgentRecord = z.infer; + +export class AgentRegistry { + private cache: Map = new Map(); + private loaded = false; + private filePath: string; + + constructor(filePath?: string) { + this.filePath = + filePath ?? + path.join( + process.cwd(), + "packages", + "server", + "agents.json" + ); + } + + async load(): Promise { + if (this.loaded) { + return Array.from(this.cache.values()); + } + try { + const content = await fs.readFile(this.filePath, "utf8"); + const parsed = JSON.parse(content); + if (!Array.isArray(parsed)) { + throw new Error("Invalid agents.json format"); + } + const records: StoredAgentRecord[] = []; + for (const entry of parsed) { + try { + const record = STORED_AGENT_SCHEMA.parse(entry); + records.push(record); + this.cache.set(record.id, record); + } catch (error) { + console.error("[AgentRegistry] Skipping invalid record:", error); + } + } + this.loaded = true; + return records; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + this.loaded = true; + this.cache.clear(); + return []; + } + console.error("[AgentRegistry] Failed to load agents:", error); + this.loaded = true; + this.cache.clear(); + return []; + } + } + + async list(): Promise { + return this.load(); + } + + async upsert(record: StoredAgentRecord): Promise { + await this.load(); + this.cache.set(record.id, record); + await this.flush(); + } + + async remove(agentId: string): Promise { + await this.load(); + this.cache.delete(agentId); + await this.flush(); + } + + async recordConfig( + agentId: string, + provider: AgentProvider, + cwd: string, + config?: SerializableAgentConfig + ): Promise { + await this.load(); + const now = new Date().toISOString(); + const existing = this.cache.get(agentId); + const updated: StoredAgentRecord = { + id: agentId, + provider, + cwd, + createdAt: existing?.createdAt ?? now, + updatedAt: now, + title: existing?.title ?? null, + lastStatus: existing?.lastStatus ?? null, + lastModeId: existing?.lastModeId ?? null, + config: config ? sanitizeConfig(config) : existing?.config, + persistence: existing?.persistence ?? null, + }; + this.cache.set(agentId, updated); + await this.flush(); + } + + async applySnapshot(snapshot: AgentSnapshot): Promise { + await this.load(); + const now = new Date().toISOString(); + const existing = this.cache.get(snapshot.id); + if (!existing) { + const record: StoredAgentRecord = { + id: snapshot.id, + provider: snapshot.provider, + cwd: snapshot.cwd, + createdAt: now, + updatedAt: now, + title: null, + lastStatus: snapshot.status, + lastModeId: snapshot.currentModeId ?? null, + config: null, + persistence: snapshot.persistence ?? null, + }; + this.cache.set(snapshot.id, record); + await this.flush(); + return; + } + const updated: StoredAgentRecord = { + ...existing, + provider: snapshot.provider, + cwd: snapshot.cwd, + updatedAt: now, + lastStatus: snapshot.status, + lastModeId: snapshot.currentModeId ?? null, + persistence: snapshot.persistence ?? existing.persistence ?? null, + }; + this.cache.set(snapshot.id, updated); + await this.flush(); + } + + async setTitle(agentId: string, title: string): Promise { + await this.load(); + const record = this.cache.get(agentId); + if (!record) { + return; + } + this.cache.set(agentId, { ...record, title }); + await this.flush(); + } + + private async flush(): Promise { + const payload = JSON.stringify(Array.from(this.cache.values()), null, 2); + await fs.mkdir(path.dirname(this.filePath), { recursive: true }); + await fs.writeFile(this.filePath, payload, "utf8"); + } +} + +function sanitizeConfig( + config: SerializableAgentConfig | undefined +): SerializableAgentConfig | undefined { + if (!config) { + return undefined; + } + const cleaned: SerializableAgentConfig = {}; + if (config.modeId) cleaned.modeId = config.modeId; + if (config.model) cleaned.model = config.model; + if (config.extra) cleaned.extra = JSON.parse(JSON.stringify(config.extra)); + return cleaned; +}