@aidex/admin
v1.1.0
Published
Aidex Admin — framework-agnostic composition layer over @aidex/connections, @aidex/ai-control, and @aidex/observability. Owns no state itself; powers the AI Control Center UI shipped as @aidex/admin-elements, @aidex/admin-react, @aidex/admin-angular, and
Downloads
576
Maintainers
Readme
@aidex/admin
Installation
pnpm add @aidex/adminnpm install @aidex/adminFramework-agnostic composition layer over @aidex/connections,
@aidex/ai-control, and @aidex/observability. AdminController owns no
state of its own — every read and command goes straight to the same
manager/control/bus instances your application already handed to Aidex.
This is the data/composition layer the AI Control Center UI is built on —
@aidex/admin-elements, @aidex/admin-react, @aidex/admin-angular, and
@aidex/admin-vue all render AdminController directly; no UI is
implemented in this package itself. See ADR-003
and the "UI packages" section below.
Usage
import { Aidex } from '@aidex/core';
import { ConnectionManager } from '@aidex/connections';
import { AIFeatureControlPlugin, InMemoryAIFeatureControl } from '@aidex/ai-control';
import { ObservabilityBus } from '@aidex/observability';
import { AdminController } from '@aidex/admin';
// 1. Application creates a ConnectionManager
const connectionManager = new ConnectionManager();
connectionManager.registerProviderFactory('gemini', (config) => new GeminiProvider(config));
connectionManager.register({ id: 'primary', providerType: 'gemini', config: { apiKey: process.env.GEMINI_KEY } });
// 2. Application creates an AIFeatureControl
const aiControl = new InMemoryAIFeatureControl();
// 3. Application creates Aidex, wiring the same aiControl into the plugin
const observability = new ObservabilityBus();
const provider = connectionManager.resolve('primary');
const aidex = new Aidex({ provider, plugins: [new AIFeatureControlPlugin(aiControl)] });
// 4. Application creates AdminController using the SAME instances —
// Admin and Aidex share state; Admin never copies it into a parallel store.
const admin = new AdminController({ connectionManager, aiControl, observability });
// 5. Admin reads a snapshot — safe to serialize, log, or send to any UI
const snapshot = admin.getSnapshot();
// { connections, aiControl, observability, health, providers, executions }
// 6. Admin changes AI state — observed by aidex.execute() on the very next call
// The optional last argument (`actor`) is attached to the resulting audit
// event when observability is wired — Admin has no auth model of its own,
// this is just a label your application's own auth layer supplies.
admin.setAIEnabled(false, 'user-42');
admin.setFeatureEnabled('text-generation', true); // per-feature override
// 7. Admin manages connections — through the same ConnectionManager Aidex's provider came from
admin.disableConnection('primary');
admin.registerConnection({ id: 'fallback', providerType: 'gemini', config: { apiKey: '...' } });
// 8. Admin discovers provider capabilities and routing — resolved live from
// each connection via ConnectionManager, never a config/secret leak
snapshot.providers;
// [{ connectionId: 'primary', providerType: 'gemini', enabled: true,
// capabilities: { 'text-generation': true, 'model-routing': false, ... } }]
// 9. Admin reads recent executions — requested-vs-actual model/provider/
// routing, tokens, cost, duration, success/error, grouped by executionId
// from ObservabilityBus's existing event stream. `routing` is present only
// when that execution went through @aidex/gateway's GatewayExecutor with
// this same ObservabilityBus wired in — see "Gateway routing/attempt
// observability" below.
snapshot.executions;
// [{ executionId, provider: 'openrouter', requestedModel: 'openrouter/auto',
// actualModel: 'anthropic/claude-3-haiku', actualProvider: 'anthropic',
// routingMode: 'auto', durationMs, tokens, totalCostUsd, success, errorCode,
// routing: { candidatesConsidered, selected, attempts: [{ candidate, outcome, ... }] } }]
// React to changes (fires after every command, and after every
// observability event when an ObservabilityBus was supplied)
const unsubscribe = admin.subscribe((next) => console.log(next.health));Persistence (optional)
AdminController still owns no state (see ADR-003) — but an application
that wants a persisted or last-known-good admin view can opt into
AdminSnapshotStore, a small storage-agnostic port @aidex/admin also
exports:
import { FileAdminSnapshotStore } from '@aidex/admin';
const store = new FileAdminSnapshotStore('./admin-snapshot.json');
admin.subscribe((snapshot) => void store.save(snapshot));MemoryAdminSnapshotStore (in-process, no I/O) is also provided as a
default/test double. AdminSnapshot is already safe to serialize by
construction, so neither implementation needs its own redaction pass.
Applications targeting a non-Node runtime, or wanting a database instead,
implement AdminSnapshotStore themselves — nothing in AdminController
references it.
Gateway routing/attempt observability
Wire the same ObservabilityBus instance into both GatewayExecutor
(@aidex/gateway) and AdminController, and each execution's routing
field fills in automatically — no separate wiring, no second event bus,
no polling:
import { GatewayExecutor } from '@aidex/gateway';
const observability = new ObservabilityBus(); // the one bus, shared
const executor = new GatewayExecutor({ retryPolicy, fallbackPolicy, resolveProvider, observability });
const admin = new AdminController({ connectionManager, aiControl, observability });
await executor.execute({ candidates, prompt });
admin.getSnapshot().executions[0].routing;
// { candidatesConsidered: [{ providerType, modelId }, ...],
// selected: { providerType, modelId } | undefined,
// attempts: [{ candidate, outcome: 'retry'|'fallback'|'success'|'exhausted',
// errorType?, retryable?, durationMs, startedAt, attempt? }, ...] }Notes for anyone extending this:
executionIdis the only correlation key — the same one every other Admin/observability/Gateway concept already uses. No second identifier was introduced.- Sanitized by construction, not by a redaction pass.
routingcarries exactly@aidex/observability's ownRoutingDecisionMetadata/AttemptMetadatafields — candidate identity (providerType+modelId), outcome, timing, anderrorTypeas a class name string only. Prompts, responses, raw provider errors, and connection config were never emitted onto these events in the first place (Row 12's own guarantee); Admin adds no additional exposure on top. - Incremental and order-tolerant. A
ROUTINGevent and any number ofATTEMPTevents for the sameexecutionIdcan arrive in any order — each is folded into the sameExecutionRecordwithout overwriting data the other already contributed. Attempts accumulate in arrival order, which is alwaysGatewayExecutor's own dispatch order. routingis absent, notnullor empty, for any execution that never went through a Gateway with observability wired in — a directStrategy/Enginecall, or aGatewayExecutorcall with noObservabilityBusconfigured, looks exactly as it did before this feature existed.- Retention is in-memory and bounded to the same 50 most-recent
executions
ExecutionRecords already were (see "Executions" below) — routing/attempt data introduces no separate retention policy or unbounded growth. State is process-local unless an application wires its ownAdminSnapshotStore; long-term/external persistence remains a host application concern, not something@aidex/adminprovides. - Still framework-agnostic. Nothing above touches
admin-react/-elements/-angular/-vue— the new fields flow through the existingAdminSnapshottype those bindings already consume structurally.
Guardrail denial observability (Row 14B)
The same shared-bus wiring above also surfaces a guardrail denial —
GatewayExecutor.executeWithGuardrails() now emits a 'guardrail'
observability event, synchronously, before GuardrailDeniedError is
thrown:
await executor.executeWithGuardrails({ candidates, prompt }); // throws GuardrailDeniedError
admin.getSnapshot().executions[0].guardrailDenial;
// { stage: 'input' | 'output', code?: string }codeonly — never the guardrail'sreason. A denying guardrail'sreasonis a free-form string an application-supplied guardrail could in principle put anything into;codeis a short, structural classification token by every built-in guardrail's own convention ("content_too_large","invalid_json", ...). This mirrorsAttemptMetadata.errorType(a class name, nevererror.message) exactly. The thrownGuardrailDeniedErroritself is completely unchanged — it still carries the fullreason/codeto the caller; only the observability event is more conservative.- No guardrail name/identity is attached, because
@aidex/guardrails' owncomposeGuardrails()doesn't track which guardrail (by.name) produced adeny(unlike itswarnpath, which does) — nothing was invented to fill that gap. - At most one per execution. An input denial happens before any
provider is dispatched; an output denial only runs after a provider
already succeeded —
executeWithGuardrails()can never produce both for the same call, soguardrailDenialis a single optional field, not a collection. - Same
executionIdcorrelation, same order tolerance, same 50-execution retention bound, same zero-new-dependency approach as the routing/attempt work above —GuardrailDenialMetadata/ObservabilityEventName.GUARDRAILlive in@aidex/observability, which both@aidex/gatewayand@aidex/adminalready depended on.
UI packages
@aidex/admin never renders anything itself — no UI, no DOM, no framework
dependency. The AI Control Center this data foundation powers ships as:
@aidex/admin-elements— a framework-independent Web Components implementation (<aidex-control-center>and its composed sections: Overview, Executions, Routing, Guardrails, Connections, Providers, AI Control, Observability, plus a shared Execution Inspector), usable from any framework or vanilla JS.@aidex/admin-react— the same shell and sections as real React components (ControlCenter,ExecutionInspector,RoutingDetail,GuardrailActivity, and more).@aidex/admin-angular/@aidex/admin-vue— thin adapters that host<aidex-control-center>inside an Angular/Vue application rather than reimplementing it natively.
All four consume the exact same AdminController instance this package
constructs — see each package's own README for its rendering API.
Design notes
- Composition, not ownership.
AdminControllerholds references to your existingConnectionManager/AIFeatureControl/ObservabilityBus— it never copies their state into a second store. See ADR-003. AdminSnapshotis always safe to serialize.connectionsis exactlyConnectionManager.list()'s output (Connectionhas noconfigfield — a structural guarantee, not a redaction pass);aiControlhas no knowledge of providers at all; the observability summary is a pure numeric reduction that never echoes raw event payloads;providersreads only a resolved provider's publicgetCapabilities();executionsreads onlyObservabilityBus's own event metadata and never a raw error message (onlyerror.code/error.name).- Provider capability discovery resolves each connection's
ProviderviaConnectionManager.resolve()and readsgetCapabilities()when it implementsCapableProvider(@aidex/providers) — a connection that fails to resolve (disabled, unregisteredproviderType) getsresolutionErrorinstead of throwing, so one bad connection never breaks the snapshot. - Executions are derived, not stored — every
ObservabilityBusevent carrying a matchingmetadata.executionId(emitted by every@aidex/providersimplementation) is folded into oneExecutionRecord. Bounded to the most recent 50 executions Admin has observed since construction (500 tracked internally) — the same accepted "can't see before construction" limitation asObservabilitySummary.lastEventAt. - Gateway routing/attempts follow the identical derivation — see
"Gateway routing/attempt observability" above. No new dependency on
@aidex/gatewaywas needed: the metadata shapes (RoutingDecisionMetadata/AttemptMetadata/RoutingCandidateInfo/AttemptOutcome) already live in@aidex/observability, which@aidex/adminalready depended on. - Guardrail denials follow the same derivation too — see "Guardrail
denial observability" above.
guardrailDenialcarries onlystage/code, never the guardrail's own free-formreason. - Every mutating command emits an audit-friendly
'admin'observability event ({ action, actor, ...detail }) when anObservabilityBusis wired — one single-notify path (commit()), so subscribers still fire exactly once per command. - Correctness never depends on
subscribe(). Every command works identically with zero subscribers;getSnapshot()always reflects live state.
