UNPKG

naisys

Version:

NAISYS - Autonomous AI agent runner with built-in context management and cost tracking

224 lines 8.82 kB
import { ADMIN_USERNAME, determineAgentStatus, pushToArrayMap, } from "@naisys/common"; import { loadAgentConfigs } from "@naisys/common-node"; import { HubEvents, UserListResponseSchema } from "@naisys/hub-protocol"; /** Loads agent configs from yaml files or receives them pushed from the hub */ export function createUserService(hubClient, promptNotificationService, hostService, startupAgentPath) { let userMap; let usersReadyPromise; init(); function init() { if (hubClient) { // Register handler for pushed user list from hub let resolveUsers; let rejectUsers; usersReadyPromise = new Promise((resolve, reject) => { resolveUsers = resolve; rejectUsers = reject; }); hubClient.registerEvent(HubEvents.USERS_UPDATED, (data) => { try { const response = UserListResponseSchema.parse(data); if (!response.success) { rejectUsers(new Error(response.error || "Failed to get user list from hub")); return; } const newMap = parseUserList(response); // The hub doesn't know about ephemerals, so carry them forward. if (userMap) { for (const user of userMap.values()) { if (user.isEphemeral) newMap.set(user.userId, user); } } userMap = newMap; resolveUsers(); } catch (error) { rejectUsers(error instanceof Error ? error : new Error(String(error))); } }); } else { userMap = loadAgentConfigs(startupAgentPath || ""); usersReadyPromise = Promise.resolve(); } } /** Wait for the user list to be received (resolves immediately in standalone mode) */ function waitForUsers() { return usersReadyPromise; } function getUsers() { return Array.from(userMap.values()); } /** Like getUsers, but hides ephemerals from anyone other than their parent. * Use for any per-agent display so siblings can't see each other's subagents. */ function getVisibleUsers(perspectiveUserId) { return Array.from(userMap.values()).filter((u) => { if (!u.isEphemeral) return true; return (u.leadUserId === perspectiveUserId || u.userId === perspectiveUserId); }); } function getUserById(id) { return userMap.get(id); } /** In hub mode, we just start the admin user, otherwise we start all lead agents */ function getStartupUserIds() { const adminUser = getUserByName(ADMIN_USERNAME); const adminId = adminUser?.userId ?? 0; const notify = (userId, message) => { promptNotificationService.notify({ wake: "always", userId, commentOutput: [message], }); }; if (hubClient) { notify(adminId, `No agents running yet. Supervisor/Hub will start agents on demand. The admin shell can be used for diagnostics.`); return [adminId]; } const leadAgents = Array.from(userMap.values()).filter((u) => !u.leadUserId && u.userId !== adminId); if (leadAgents.length === 0) { notify(adminId, `No agent param include at startup so you are in the admin shell which can be used for diagnostics.`); return [adminId]; } // In standalone mode with a single agent, don't start admin as it would // require the user to exit twice and prevent ns-session complete from ending the app if (leadAgents.length === 1) { return [leadAgents[0].userId]; } // Multiple agents: always include admin so that all agents can be turned off without ending process const agentList = leadAgents.map((u) => u.username).join(", "); leadAgents.forEach((agent) => notify(agent.userId, `Multiple agents started: ${agentList}`)); return [...leadAgents.map((u) => u.userId), adminId]; } // Active user tracking (driven by heartbeatService) let activeUserIds = new Set(); let userHostIds = new Map(); function setActiveUsers(hostActiveAgents) { const newActiveUserIds = new Set(); const newUserHostIds = new Map(); for (const [hostIdStr, userIds] of Object.entries(hostActiveAgents)) { const hostId = Number(hostIdStr); for (const userId of userIds) { newActiveUserIds.add(userId); if (hostIdStr) pushToArrayMap(newUserHostIds, userId, hostId); } } activeUserIds = newActiveUserIds; userHostIds = newUserHostIds; } function isUserActive(userId) { return activeUserIds.has(userId); } function getUserHostIds(userId) { return userHostIds.get(userId) ?? []; } function getUserHostNames(userId) { const hostIds = userHostIds.get(userId) ?? []; return hostIds .map((id) => hostService.getHostName(id)) .filter((name) => !!name); } function getUserHostDisplayNames(userId) { const hostIds = userHostIds.get(userId) ?? []; const localHostId = hostService.getLocalHostId(); return hostIds .map((id) => id === localHostId ? "(local)" : hostService.getHostName(id)) .filter((name) => !!name); } function getUserStatus(userId) { if (!hubClient) return "Available"; const user = userMap.get(userId); const status = determineAgentStatus({ isActive: isUserActive(userId), isPaused: false, isEnabled: user?.enabled ?? true, isSuspended: false, assignedHostIds: user?.assignedHostIds, isHostOnline: hostService.isHostActive, hasNonRestrictedOnlineHost: hostService.hasNonRestrictedOnlineHost(), }); return (status.charAt(0).toUpperCase() + status.slice(1)); } /** Parse a UserListResponse into a userId → UserEntry map */ function parseUserList(response) { const map = new Map(); for (const user of response.users ?? []) { map.set(user.userId, { userId: user.userId, username: user.username, enabled: user.enabled, leadUserId: user.leadUserId, assignedHostIds: user.assignedHostIds, config: user.config, }); } return map; } function getUserByName(username) { for (const user of userMap.values()) { if (user.username === username) { return user; } } return undefined; } /** Synthetic id for an ephemeral subagent. Negative values can't collide * with hub-assigned positive ids, and double as the entry's `subagentId`. */ let nextSyntheticId = 0; function nextSyntheticUserId() { nextSyntheticId -= 1; return nextSyntheticId; } /** Add an ephemeral user that lives only in local memory; never sent to the hub. */ function addLocalUser(entry) { if (userMap.has(entry.userId)) { throw `User id ${entry.userId} already exists`; } userMap.set(entry.userId, entry); } function removeLocalUser(userId) { userMap.delete(userId); } /** Parse a comma-separated username string and resolve to users. Throws on any not-found. */ function resolveUsernames(csvUsernames) { const usernames = csvUsernames.split(",").map((u) => u.trim()); const resolved = []; const errors = []; for (const username of usernames) { const user = getUserByName(username); if (!user) { errors.push(`'${username}' not found`); } else { resolved.push(user); } } if (errors.length > 0) { throw `Error: ${errors.join("; ")}`; } return resolved; } return { getUsers, getVisibleUsers, getUserById, waitForUsers, getStartupUserIds, setActiveUsers, isUserActive, getUserHostIds, getUserHostNames, getUserHostDisplayNames, getUserStatus, getUserByName, resolveUsernames, nextSyntheticUserId, addLocalUser, removeLocalUser, }; } //# sourceMappingURL=userService.js.map