@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
211 lines • 10.1 kB
TypeScript
/**
* Guardrails on the agent loop's own seams: an {@link InputProcessor} that scans (and redacts) every
* prompt — including the tool results riding in it — an {@link OutputProcessor} that scans every
* answer and the tool calls it asks for, a tool-handler wrapper, and a tool-definition screen.
*
* Built only on the processor SPI, so it is a consumer of the loop rather than a part of it: the
* same rules and detectors run standalone through `scan` for traffic that never meets the loop.
*/
import type { InputProcessor, OutputProcessor } from '../spi/processors.js';
import type { ToolHandler } from '../spi/tool.js';
import type { Actor } from '../types.js';
import { type ToolDefinitionText } from './detectors/tool-poisoning.js';
import { type GuardHit, type ScanOptions, type ScanResult } from './engine.js';
import type { Finding, GuardContext, GuardrailAction, GuardrailRule, GuardrailStage, PiiType, SecretType, Segment } from './types.js';
import { Vault } from './vault.js';
/** What a rule's `when` and a custom detector see when the guardrails run inside the loop. */
export interface AgentGuardContext extends GuardContext {
/** Undefined only for a tool definition screened outside any turn. */
threadId?: string;
actor?: Actor;
agentName?: string;
/** Model step within the run; undefined outside a processor. */
step?: number;
/** The tool server a screened definition came from (e.g. the MCP server's name). */
server?: string;
}
type Stages = readonly GuardrailStage[];
export interface PiiShorthand {
action: GuardrailAction;
types?: readonly PiiType[];
/**
* Put the original values back into the answer and into tool arguments (the model only sees
* placeholders). Covers values from the prompt and from tool results. Default true.
*/
restore?: boolean;
stages?: Stages;
}
export interface SecretsShorthand {
action: GuardrailAction;
types?: readonly SecretType[];
stages?: Stages;
}
export interface InjectionShorthand {
/** Default `block`. */
action?: GuardrailAction;
/** Noisy-OR score (0-1) at which the heuristics fire. Default 0.5. */
threshold?: number;
stages?: Stages;
}
export interface ToolPoisoningShorthand {
/** `block` (default) refuses the tool in {@link Guardrails.screenTool}; `log` only reports it. */
action?: 'block' | 'log';
threshold?: number;
}
/** One decision worth auditing. Never carries the matched values themselves. */
export interface GuardrailEvent {
stage: GuardrailStage;
/** The effective action (`approve` is reported as it was configured, and enforced as a block). */
action: GuardrailAction;
decisive?: {
id: string;
name: string;
message: string;
};
/** Hits in the newest content only — history re-sent every step is not reported again. */
hits: GuardrailEventHit[];
redactions: number;
latencyMs: number;
context: AgentGuardContext;
}
export type GuardrailEventHit = Omit<GuardHit, 'values'> & {
/** {@link GuardrailsOptions.fingerprint} of each distinct value; empty without one. */
fingerprints: string[];
};
/** Where a thread's reversible placeholders live between the prompt and the answer. */
export interface VaultStore {
load(threadId: string): Vault | undefined | Promise<Vault | undefined>;
save(threadId: string, vault: Vault): void | Promise<void>;
}
/**
* The default {@link VaultStore}: this process's memory, bounded to the most recent threads. A run
* resumed in ANOTHER process finds no vault, so its placeholders reach the reader unrestored — the
* values still never reach the model. Supply a shared store (with {@link Vault.toJSON}) to restore
* across processes; it then holds the raw values.
*/
export declare class InMemoryVaultStore implements VaultStore {
private readonly maxThreads;
private readonly vaults;
constructor(maxThreads?: number);
load(threadId: string): Vault | undefined;
save(threadId: string, vault: Vault): void;
}
export type GuardrailRulesSource = readonly GuardrailRule<AgentGuardContext>[] | ((ctx: AgentGuardContext) => readonly GuardrailRule<AgentGuardContext>[] | Promise<readonly GuardrailRule<AgentGuardContext>[]>);
export interface GuardrailsOptions {
/** PII (email, phone, CPF/CNPJ, SSN, cards with Luhn, IBAN, IPv4). */
pii?: GuardrailAction | PiiShorthand;
/** API keys, tokens and private keys. */
secrets?: GuardrailAction | SecretsShorthand;
/** Prompt-injection / jailbreak heuristics. */
injection?: GuardrailAction | InjectionShorthand;
/** Screen tool definitions for hidden instructions — see {@link Guardrails.screenTool}. */
toolPoisoning?: boolean | ToolPoisoningShorthand;
/**
* More rules, evaluated alongside the shorthands. A function is called per scan with the turn's
* context, which is the seam for per-tenant policy: look the tenant's rules up by `ctx.actor`.
*/
rules?: GuardrailRulesSource;
/** Audit hook. Awaited; a throw is swallowed so an audit sink can never change a decision. */
onEvent?: (event: GuardrailEvent) => void | Promise<void>;
/** Keyed hash (or any stable digest) of a matched value, reported on events instead of it. */
fingerprint?: (value: string) => string;
/** Per-stage refusal messages for rules that set none. */
messages?: ScanOptions['messages'];
/** Where reversible placeholders live between prompt and answer. Default {@link InMemoryVaultStore}. */
vaults?: VaultStore;
/**
* Keep the turn streaming: the output processor declares `incremental` with this lookback, so
* the loop releases a prefix while holding the last `lookbackChars` characters. `false` gates the
* whole answer instead. Default `{ lookbackChars: 256 }` — wide enough for every built-in
* pattern and placeholder; widen it for custom patterns that match longer text.
*/
incremental?: false | {
lookbackChars?: number;
};
/** Processor name (user-visible on a refusal). Default `guardrails`. */
name?: string;
}
/** A guardrail refused content the loop cannot rewrite around: a prompt, or a tool call. */
export declare class GuardrailBlockedError extends Error {
readonly stage: GuardrailStage;
/** The message for the person — the rule's own, or the stage default. */
readonly userMessage: string;
readonly rule?: {
id: string;
name: string;
} | undefined;
constructor(stage: GuardrailStage,
/** The message for the person — the rule's own, or the stage default. */
userMessage: string, rule?: {
id: string;
name: string;
} | undefined);
}
/** The rules the shorthand options stand for. Exported so a caller can inspect or extend them. */
export declare function shorthandRules(options: GuardrailsOptions): GuardrailRule<AgentGuardContext>[];
/**
* Guardrails for the agent loop. Register the two processors, optionally wrap tool handlers, and
* hand {@link screenTool} to whatever imports remote tool definitions:
*
* ```ts
* const guardrails = createGuardrails({ pii: 'redact', secrets: 'block', injection: { threshold: 0.6 }, toolPoisoning: true });
* // config/agent.ts
* defineConfig({ inputProcessors: [guardrails.input], outputProcessors: [guardrails.output], … });
* ```
*/
export declare class Guardrails {
private readonly options;
/** Scans every prompt; redacts in place, withholds tool results, throws on a blocked prompt. */
readonly input: InputProcessor;
/** Scans every answer and the tool calls it asks for; redacts, restores placeholders, or rejects. */
readonly output: OutputProcessor;
private readonly shorthand;
private readonly vaults;
/** Per thread: hits already reported this turn, so re-scans of the same content stay quiet. */
private readonly reported;
constructor(options?: GuardrailsOptions);
/** Every rule in force for `ctx`: the shorthands, then {@link GuardrailsOptions.rules}. */
rulesFor(ctx: AgentGuardContext): Promise<GuardrailRule<AgentGuardContext>[]>;
/**
* Scans arbitrary segments at one stage with the configured rules, reporting through `onEvent`.
* What the processors use; also the entry point for traffic that never meets the loop.
*/
scan(ctx: AgentGuardContext, segments: readonly Segment[], vault?: Vault): Promise<ScanResult>;
/**
* Screens one tool definition (name, title, description, and every description in its input
* schema) for hidden instructions. `allowed: false` means a `block`/`approve` rule fired — leave
* the tool out of the catalog. Shaped to be handed straight to an MCP importer's screen hook.
*/
screenTool(tool: ToolDefinitionText & {
server?: string;
}): Promise<{
allowed: true;
} | {
allowed: false;
reason: string;
}>;
/**
* Wraps a tool handler so its arguments are guarded where the loop cannot rewrite them: the
* thread's placeholders are restored (the model only ever saw `[EMAIL_1]`, the tool needs the
* address), then the `tool_args` rules run — `redact` rewrites what the tool receives, `block`
* throws a {@link GuardrailBlockedError}, which the model reads as the tool's failure.
*/
wrapTool<I, O>(toolName: string, handler: ToolHandler<I, O>): ToolHandler<I, O>;
private contextOf;
private processInput;
private guardToolResult;
private processOutput;
private report;
}
/** Shorthand for `new Guardrails(options)`. */
export declare function createGuardrails(options?: GuardrailsOptions): Guardrails;
/**
* The tool-poisoning screen alone — no rules, no events. `score` is the noisy-OR of the signals
* that fired; `findings` is empty below `threshold` (default 0.5).
*/
export declare function screenToolDefinition(tool: ToolDefinitionText, threshold?: number): {
score: number;
findings: Finding[];
};
export {};
//# sourceMappingURL=agent.d.ts.map