Aviary
Concepts

Governance

Four layers stack on every turn — who's calling and what they may do, how many tokens they've spent today, what it cost, and an audit trail of everything that happened.

"Governance" isn't one feature — it's four independent layers the loop applies to every turn. Each has its own SPI, its own configuration point, and its own guide; none of them requires the others.

LayerWhat it doesWhere configuredGuide
Identity + authorizationResolves the actor from the request — an anonymous identity per browser by default, your login when configured; gates each tool by role intersection or a delegated abilityactorResolver, defaultRoles, emptyRoles or rolesPolicy on AgentModule.forRoot, plus per-tool roles/abilityIdentity & Authorization
QuotaRefuses a send (429) while the actor's budget — day/month, tokens/USD — is used upquota: { limits } or quota: yourQuotaProvider on AgentModule.forRoot; omitted → reported, never enforcedCost & Governance
CostA usage row per model call, rolled into spend per model/actor/day; a gateway's reported cost wins, else a cache-aware token estimateThe store's AgentGovernanceQueries + a versioned AgentModelPricing tableCost & Governance
ReliabilityEvery run's start/end is durably recorded (success/failure, duration, retries, promptHash), independent of thread historyAutomatic once the bound AgentStore implements the optional recording SPI (both bundled stores do)Cost & Governance
ApprovalsWho must approve an action call and for how long (ApprovalPolicy); per-call preflight checks; a cross-thread inbox of blocking calls awaiting a decision; optionally, independent proposals that don't hold the turnapprovalPolicy and actionApprovalMode on AgentModule.forRoot; AGENT_APPROVAL_PORT — bound automatically by AgentModuleHuman-in-the-loop & Durability, Action proposals
AuditEvery stage of a run announced on Node's diagnostics_channel, plus tracing spans for a per-run waterfallNothing to configure — aviary:agent:* always fires; consumed by Telescope or your own subscriberCost & Governance

Identity + authorization

Two questions gate every tool call: who is calling, and may they call this. Without an actorResolver every browser is its own anonymous actor (an HttpOnly cookie), so two visitors never share an identity; configure one to require login. Once an actor is resolved, each tool declares exactly one gate: roles (a tool with none declared reaches the policy as roles: undefined, and DefaultRolesPolicy applies defaultRoles there — an empty list is open unless emptyRoles: 'deny' binds ClosedRolesPolicy) or ability (delegated to a nestjs-authz Gate by AuthzRolesPolicy from -authz, which AgentModule picks up in either import order; without it, the built-in policies refuse a tool that has an ability and no roles). A handler's own isEnabled() and canUse(actor) run alongside the policy, and a persona's allowedTools narrows the set further. All of it runs server-side, both when the turn's tool list is built and again on invoke, before the tool's execute is ever called — a model can ask for anything, it can't talk its way past the gate.

The same "the agent never fabricates a caller" principle carries into the governance endpoints: POST /agent/tool-call/approve · /reject and the /agent/threads/:id family resolve the acting actor and assert they own the target thread/tool-call — someone else's returns 403, a missing one 404.

Quota

A per-actor budget across windows (day, month; tokens and/or USD), read off the usage ledger by the default LedgerQuotaProvider or supplied by your own QuotaProvider. GET /agent/quota reports it; with quota configured, a send is refused with 429 while a window is exhausted. Tokens count inputTokens + outputTokens — unaffected by the cache-token breakdown that only refines cost.

Cost

Every model call appends a usage row regardless of outcome. Cost is resolved per row: a gateway's real reported cost (Vercel AI Gateway, OpenRouter) wins when present; otherwise the estimate accounts for prompt caching (cache writes at a premium, cache reads at a discount) against the current pricing row for that model. AgentGovernanceQueries rolls the rows up into spend by model, spend by actor, a daily trend, and recent activity — the same read-model the standalone dashboard and the Telescope "Agent" tab both consume.

Reliability

Cost and quota answer "what did this cost"; reliability answers "did the run actually succeed." Every run's start and end are recorded through an optional AgentStore surface (recordRunStart/recordRunEnd/bumpRunRetries — a store that doesn't implement it degrades to no reliability data, not an error), including the sha256 promptHash of the run's resolved system prompt, so an error-rate shift can be correlated with a prompt change. AgentGovernanceQueries exposes the rollups — aggregate (runMetrics, runsByAgent, runErrors, runTrend) and per-run (recentRuns, and a paginated/filterable runsPage) — consumed by the same two surfaces as cost: the standalone dashboard's Reliability section and Telescope's Agent tab. See Architecture for how a run's outcome gets recorded when its steps are dispatched across the fleet.

Approvals inbox

Whether an action call needs approval at all, who may decide it ('requester' — the thread's owner — or a role), and how long the request stays open (ttlMs, after which it settles expired) is the ApprovalPolicy's call; the default asks the requester, with no expiry. Before the card is shown and again right before execution, the tool's own preflight can deny the call, report it already done, or supply per-call confirmation wording — no approval, remembered or not, skips the execution-time check.

An action tool's suspend (see The agent loop and Human-in-the-loop & Durability) is normally decided from the thread it happened in. The approvals inbox is the cross-thread view of the same decision: every tool call sitting pending_approval, across every thread and actor, oldest first (AgentGovernanceQueries.pendingApprovals). The optional AGENT_APPROVAL_PORT SPI (AgentApprovalPort) — bound automatically by AgentModule/AgentDurableModule — routes a console-side approve/reject through the exact same decision path a chat approval uses (a durable signal, or inline resolution), without re-deriving thread ownership; the console's own guards are expected to front that authorization instead. Decision carries an optional executedByRef, so the loop can persist who actually decided (decision.executedByRef ?? the run's actor) — the dashboard's approvalActorRef option stamps it from the live request, defaulting to the same ActorResolver chat already uses. No port bound → the API answers 501 and the console renders read-only rather than silently no-op-ing an approval.

With actionApprovalMode: 'independent', an action call becomes an action proposal instead of a parked turn: the turn finishes, and the proposal is decided later — over GET <base>/threads/:threadId/action-proposals and POST <base>/threads/:threadId/action-proposals/:proposalId/approve · /reject, a text command in the thread, or the port's approveActionProposal / rejectActionProposal (which require an actual actor and an explicit thread/proposal target). A worker executes approved proposals under the requester's current identity and permissions. Proposals are not pending_approval tool calls, so they don't appear in pendingApprovals. See Action proposals.

Audit

Every stage of a run — start, message, tool call, delegation, retrieval, quota exceeded, failure, finish — is announced on Node's diagnostics_channel under aviary:agent:*. This is unconditional instrumentation, not an opt-in: the library doesn't instrument your code, but it always announces its own. Alongside those point events, every run also emits tracing spans (llm.turn/tool.execution/retrieval/follow-ups, correlated by traceId = runId) so a single turn renders as a waterfall rather than a flat list — see Architecture. Telescope consumes both the channel (for live activity and per-tool stats, toolStats) and the spans (for the waterfall); any other subscriber (your own metrics, an alerting rule) can tap the point events the same way.

Where to go next

On this page