UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

118 lines (117 loc) • 5.44 kB
import { existsSync, mkdirSync, readFileSync } from "node:fs"; import { basename, join, resolve } from "node:path"; import { entryAccessible } from "../../agents/roster-store.js"; import { normalizeAgentName } from "../../core/task-registry.js"; import { atomicWriteFile } from "../file/fs-atomic.js"; import { withCcLock } from "./lockfile.js"; export function createCcTeamsRosterAdapter(opts) { const scope = opts.scope ?? "default"; if (scope !== "default") { throw new Error(`CcTeamsRosterAdapter: CC team files carry no tenant axis — this adapter is licensed for single-tenant scope:"default" deployments only (D-1); got scope ${JSON.stringify(scope)}`); } const dir = resolve(opts.teamDir); const configPath = join(dir, "config.json"); const now = opts.now ?? Date.now; const load = () => { try { return JSON.parse(readFileSync(configPath, "utf8")); } catch (err) { if (err.code === "ENOENT") return undefined; throw err; } }; const save = (config) => { atomicWriteFile(dir, configPath, JSON.stringify(config, null, 2)); }; const toEntry = (m) => ({ name: m.name, agentId: m.agentId, ...(typeof m.semaSessionId === "string" ? { sessionId: m.semaSessionId } : {}), ...(typeof m.semaToolUseId === "string" ? { toolUseId: m.semaToolUseId } : {}), ...(typeof m.semaRootSessionId === "string" ? { rootSessionId: m.semaRootSessionId } : {}), ...(typeof m.semaModel === "string" ? { model: m.semaModel } : {}), ...(m.semaSessionScoped === true ? { sessionScoped: true } : {}), owner: typeof m.semaOwner === "string" ? m.semaOwner : opts.owner, scope, createdAt: typeof m.joinedAt === "number" ? m.joinedAt : 0, }); const absent = () => !existsSync(configPath); return { record: (entry) => { if (entry.scope !== scope) { throw new Error(`CcTeamsRosterAdapter: cannot record entry with scope ${JSON.stringify(entry.scope)} — this adapter persists scope:"${scope}" bindings only (D-1 single-tenant)`); } mkdirSync(dir, { recursive: true }); return withCcLock(configPath, () => { const config = load() ?? { name: basename(dir), createdAt: now(), members: [], }; const members = Array.isArray(config.members) ? config.members : []; const prior = members.find((m) => m.agentId === entry.agentId); const member = { ...(prior ?? {}), agentId: entry.agentId, name: entry.name, ...(entry.model !== undefined ? { semaModel: entry.model } : {}), ...(entry.sessionId !== undefined ? { semaSessionId: entry.sessionId } : {}), ...(entry.toolUseId !== undefined ? { semaToolUseId: entry.toolUseId } : {}), ...(entry.rootSessionId !== undefined ? { semaRootSessionId: entry.rootSessionId } : {}), ...(entry.sessionScoped === true ? { semaSessionScoped: true } : {}), semaOwner: entry.owner, joinedAt: entry.createdAt, }; config.members = [...members.filter((m) => m.agentId !== entry.agentId), member]; save(config); }); }, resolve: (name, access) => { if (absent()) return undefined; return withCcLock(configPath, () => { const config = load(); if (config === undefined || !Array.isArray(config.members)) return undefined; const key = normalizeAgentName(name); if (key === "") return undefined; const pool = config.members .filter((m) => typeof m.agentId === "string" && typeof m.name === "string") .map(toEntry) .filter((e) => normalizeAgentName(e.name) === key && entryAccessible(e, access)); let best; for (const e of pool) if (best === undefined || e.createdAt >= best.createdAt) best = e; return best; }); }, list: () => { if (absent()) return []; return withCcLock(configPath, () => { const config = load(); if (config === undefined || !Array.isArray(config.members)) return []; return config.members.filter((m) => typeof m.agentId === "string" && typeof m.name === "string").map(toEntry); }); }, releaseAgent: (agentId) => { if (absent()) return; return withCcLock(configPath, () => { const config = load(); if (config === undefined || !Array.isArray(config.members)) return; const next = config.members.filter((m) => m.agentId !== agentId); if (next.length === config.members.length) return; config.members = next; save(config); }); }, }; }