naisys
Version:
NAISYS - Autonomous AI agent runner with built-in context management and cost tracking
224 lines • 8.82 kB
JavaScript
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