@bernierllc/admin-agent-service
v0.1.0
Published
Orchestrator composing ai-agent-runtime, agent-tool-registry, agent-changeset, and agent-conversation-store into a configurable AdminAgent
Readme
@bernierllc/admin-agent-service
Orchestrator composing ai-agent-runtime, agent-tool-registry, agent-changeset, and agent-conversation-store into a configurable AdminAgent — the single surface consumers call for privileged LLM-powered admin sessions.
Installation
npm install @bernierllc/admin-agent-serviceQuick Start
import { createAdminAgent } from '@bernierllc/admin-agent-service';
import { InMemoryConversationStore } from '@bernierllc/agent-conversation-store';
// 1. Define your domain tools
const listBookingsTool = {
name: 'list-bookings',
description: 'Lists bookings for an organisation.',
inputSchema: { type: 'object', properties: { limit: { type: 'number' } } },
permissions: ['admin:read'],
execute: async (input, context) => ({
success: true,
message: 'Fetched bookings',
data: await db.bookings.findMany({ orgId: context.metadata?.orgId, take: input.limit }),
}),
};
// 2. Create the agent
const agent = createAdminAgent({
provider: myOpenAIProvider, // any @bernierllc/ai-provider-core AIProvider
tools: [listBookingsTool],
permissionResolver: async (ctx) => resolvePerms(ctx.roles),
store: new InMemoryConversationStore(),
});
// 3. Stream the response
for await (const event of agent.chat('List my bookings', { userId: 'u1', orgId: 'org1' })) {
if (event.type === 'text-delta') process.stdout.write(event.delta);
if (event.type === 'done') console.log('\n✓ finished');
if (event.type === 'change-summary') console.log(event.summary.plainText);
}Usage
Feature Gate
Prevent access when an org's AI feature flag is disabled:
const featureGateHook = {
isEnabled: async (feature, ctx) => featureFlags.isEnabled(feature, ctx.orgId),
};
const agent = createAdminAgent({
...config,
featureGateHook,
featureName: 'ai_admin_chat', // defaults to 'ai_chat'
});When the gate returns false, chat() yields { type: 'feature-disabled', feature: 'ai_admin_chat' } and returns.
Rate Limiting
Throttle requests per org:
const rateLimitHook = {
check: async (key, limit, windowSeconds) => redis.rateLimiter.allow(key, limit, windowSeconds),
};
const agent = createAdminAgent({
...config,
rateLimitHook,
rateLimit: 20, // max 20 requests (default)
rateLimitWindowSeconds: 60, // per 60-second window (default)
});When the limit is exceeded, chat() yields { type: 'rate-limited', retryAfterSeconds: 60 } and returns.
Custom System Prompt
const agent = createAdminAgent({
...config,
systemPromptBuilder: async (authCtx, orgCtx) => `
You are an admin assistant for ${orgCtx.name}.
User: ${authCtx.userId} | Roles: ${authCtx.roles?.join(', ')}.
Today is ${new Date().toLocaleDateString()}.
`.trim(),
});Conversation Persistence
chat() automatically:
- Calls
store.findLatest(context, orgId)to resume an existing conversation - Persists the incoming user message before starting the LLM loop
- Persists tool-call summaries after each agentic turn
- Persists the final assistant text after
done
Pass any ConversationStore adapter (SQL, Redis, Prisma, etc.) implementing the interface from @bernierllc/agent-conversation-store.
Tool Permissions
Each ToolDefinition declares permissions: string[]. The permissionResolver callback returns the current user's granted permissions. The agent-tool-registry enforces the match before executing any tool — no permission means the LLM gets an error result and continues.
const tool = {
name: 'delete-record',
permissions: ['admin:delete'], // caller must hold this permission
execute: async (input, ctx) => { /* ... */ },
};
const agent = createAdminAgent({
...config,
permissionResolver: async (ctx) => {
if (ctx.roles?.includes('super-admin')) return ['admin:read', 'admin:write', 'admin:delete'];
if (ctx.roles?.includes('admin')) return ['admin:read', 'admin:write'];
return ['admin:read'];
},
});Change Summary
After every chat() run completes, a change-summary event is emitted:
for await (const event of agent.chat('Delete that booking', auth)) {
if (event.type === 'change-summary') {
// Inject summary into UI or next LLM context
console.log(event.summary.plainText);
// "1 change: admin.delete-booking — Booking #42 deleted."
}
}Abort / Timeout
Pass an AbortSignal to cancel mid-flight:
const controller = new AbortController();
setTimeout(() => controller.abort(), 30_000); // 30-second hard limit
for await (const event of agent.chat('...', auth, controller.signal)) {
// ...
}API
createAdminAgent(config: AdminAgentConfig): AdminAgent
Factory function. Validates config synchronously and returns an AdminAgent instance.
Throws AdminAgentConfigError when required fields are missing or invalid.
AdminAgentConfig
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| provider | AIProvider | ✅ | — | LLM provider (any @bernierllc/ai-provider-core implementation) |
| tools | ToolDefinition[] | ✅ | — | Consumer-defined domain tools |
| permissionResolver | (ctx) => Promise<string[]> | ✅ | — | Maps auth context to granted permission strings |
| store | ConversationStore | ✅ | — | Conversation persistence adapter |
| conversationContext | string | | 'admin_assistant' | Label for findLatest() conversation lookup |
| persona | PersonaConfig | | — | Static persona / prompt from chat-agent-persona-manager |
| systemPromptBuilder | (auth, org) => Promise<string> | | — | Dynamic system prompt builder (overrides persona.prompt) |
| maxTurns | number | | 10 | Maximum LLM turns per chat() call |
| timeoutMs | number | | 30000 | Per-LLM-call timeout in milliseconds |
| featureGateHook | FeatureGateHook | | — | Optional feature-flag integration |
| featureName | string | | 'ai_chat' | Feature name passed to featureGateHook.isEnabled() |
| rateLimitHook | RateLimitHook | | — | Optional rate-limiting integration |
| rateLimit | number | | 20 | Max requests per window |
| rateLimitWindowSeconds | number | | 60 | Rolling window duration |
| neverhubAdapter | NeverHubAdapter | | — | Pre-configured NeverHub adapter (skips auto-detection) |
AdminAgent
interface AdminAgent {
chat(
userMessage: string,
authContext: AuthContext,
signal?: AbortSignal
): AsyncIterable<AdminAgentEvent>;
}AdminAgentEvent (union)
| type | Additional fields | Emitted when |
|--------|-------------------|--------------|
| text-delta | delta: string | LLM emits a text token |
| tool-call-start | toolName, toolCallId, input | Tool execution begins |
| tool-call-result | toolName, toolCallId, result | Tool execution completes |
| turn-complete | turnIndex, totalTokens | One LLM turn ends |
| done | finalText, totalTurns, totalTokens | All turns complete |
| error | error: AgentRuntimeError | Runtime or provider error |
| change-summary | summary: RollupSummary | After done — changeset summary |
| feature-disabled | feature: string | Feature gate returned false |
| rate-limited | retryAfterSeconds: number | Rate limit exceeded |
AuthContext
interface AuthContext {
userId: string;
orgId: string;
roles?: string[];
metadata?: Record<string, unknown>;
}Error Classes
All errors extend AdminAgentServiceError and carry an ES2022 Error.cause chain.
import {
AdminAgentServiceError,
AdminAgentConfigError,
AdminAgentFeatureDisabledError,
AdminAgentRateLimitedError,
AdminAgentPermissionError,
} from '@bernierllc/admin-agent-service';| Class | Code | Thrown when |
|-------|------|-------------|
| AdminAgentServiceError | ADMIN_AGENT_SERVICE_ERROR | Base — unexpected runtime errors |
| AdminAgentConfigError | INVALID_CONFIG | Required config fields missing |
| AdminAgentFeatureDisabledError | FEATURE_DISABLED | Feature gate hook throws unexpectedly |
| AdminAgentRateLimitedError | RATE_LIMITED | Rate limit hook throws unexpectedly |
| AdminAgentPermissionError | PERMISSION_DENIED | Permission resolver throws unexpectedly |
Note: when hooks return false (controlled denial), the service yields feature-disabled / rate-limited events instead of throwing.
Integration Documentation
NeverHub Integration
admin-agent-service includes optional NeverHub integration for telemetry and service registration. Core functionality works with or without NeverHub.
Graceful degradation: If NeverHub is unavailable, initialization failures are caught and logged. The agent continues functioning without NeverHub events.
Auto-detection (default):
// No extra config needed — NeverHub is auto-detected via environment variables:
// NEVERHUB_API_URL, NEVERHUB_ENDPOINT, or NEVERHUB_HOST
const agent = createAdminAgent(config);Pre-configured adapter (advanced):
import { NeverHubAdapter } from '@bernierllc/neverhub-adapter';
const neverhubAdapter = new NeverHubAdapter();
const agent = createAdminAgent({ ...config, neverhubAdapter });Events published:
admin_agent.conversation.started— first conversation created for an orgadmin_agent.turn.complete— each LLM turn completionadmin_agent.done— run finished with change countadmin_agent.feature_disabled— feature gate blocked requestadmin_agent.rate_limited— rate limit blocked requestadmin_agent.error— runtime error occurred
Logger Integration
Uses @bernierllc/logger for structured JSON logging. Log level is controlled by the LOG_LEVEL environment variable (default: info).
Key log events:
info: "Started new conversation"— first org conversationinfo: "Resuming existing conversation"— existing conversation foundwarn: "Feature gate blocked request"— feature-disabled pathwarn: "Rate limit exceeded"— rate-limited pathwarn: "NeverHub initialization failed (non-fatal)"— NeverHub unavailableerror: "Agent runtime error"— LLM error eventerror: "Failed to persist tool-call messages (non-fatal)"— store error during turn
License
Copyright (c) 2025 Bernier LLC
This file is licensed to the client under a limited-use license. The client may use and modify this code only within the scope of the project it was delivered for. Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
