/** * Presenter — MVA View Layer for AI Agents * * Domain-level "lens" that defines how data is presented to an LLM. * A Presenter validates data through a Zod schema, attaches JIT system * rules, and renders deterministic UI blocks — all at Node.js speed. * * The Presenter is **domain-level**, not tool-level. The same Presenter * (e.g. `InvoicePresenter`) can be reused across any tool that returns * invoices, guaranteeing consistent AI perception of the domain. * * ## Advanced Features * * - **Context-Aware**: Callbacks receive `TContext` for RBAC, DLP, locale * - **Cognitive Guardrails**: `.agentLimit()` truncates large arrays * - **Agentic Affordances**: `.suggestActions()` for HATEOAS-style hints * - **Composition**: `.embed()` nests child Presenters (DRY relational data) * * @example * ```typescript * import { createPresenter, ui } from '@vinkius-core/mcp-fusion'; * import { z } from 'zod'; * * export const InvoicePresenter = createPresenter('Invoice') * .schema(z.object({ * id: z.string(), * amount_cents: z.number(), * status: z.enum(['paid', 'pending']), * })) * .systemRules((invoice, ctx) => [ * 'CRITICAL: amount_cents is in CENTS. Divide by 100.', * ctx?.user?.role !== 'admin' ? 'Mask sensitive financial data.' : null, * ]) * .uiBlocks((invoice) => [ * ui.echarts({ * series: [{ type: 'gauge', data: [{ value: invoice.amount_cents / 100 }] }], * }), * ]) * .agentLimit(50, (omitted) => * ui.summary(`⚠️ Dataset truncated. 50 shown, ${omitted} hidden.`) * ) * .suggestActions((invoice) => { * if (invoice.status === 'pending') { * return [{ tool: 'billing.pay', reason: 'Offer immediate payment' }]; * } * return []; * }); * ``` * * @see {@link createPresenter} for the factory function * @see {@link ResponseBuilder} for manual response composition * * @module */ import { z, ZodType, type ZodRawShape } from 'zod'; import { ResponseBuilder } from './ResponseBuilder.js'; import { type UiBlock } from './ui.js'; /** * Check if a value is a {@link Presenter} instance. * * Used by the execution pipeline to detect Presenters declared * via the `returns` field in action configuration. * * @param value - Any value * @returns `true` if the value is a Presenter */ export declare function isPresenter(value: unknown): value is Presenter; /** A suggested next action for HATEOAS-style agent guidance */ export interface ActionSuggestion { /** Tool name to suggest (e.g. 'billing.pay') */ readonly tool: string; /** Human-readable reason for the suggestion */ readonly reason: string; } /** UI blocks callback — with optional context */ type ItemUiBlocksFn = (item: T, ctx?: unknown) => (UiBlock | null)[]; /** Collection UI blocks callback — with optional context */ type CollectionUiBlocksFn = (items: T[], ctx?: unknown) => (UiBlock | null)[]; /** Suggest actions callback — with optional context */ type SuggestActionsFn = (data: T, ctx?: unknown) => ActionSuggestion[]; /** * Domain-level Presenter — the "View" in MVA (Model-View-Agent). * * Encapsulates: * - **Schema** (Zod): Validates and filters data before it reaches the LLM * - **System Rules**: JIT context directives that travel with the data * - **UI Blocks**: SSR-rendered visual blocks (charts, diagrams, tables) * - **Agent Limit**: Smart truncation for large collections * - **Action Suggestions**: HATEOAS-style next-action hints * - **Embeds**: Relational Presenter composition (DRY) * * @typeParam T - The validated output type (inferred from the Zod schema) * * @see {@link createPresenter} for the factory function */ export declare class Presenter { /** @internal Brand for type detection in the pipeline */ readonly __brand: "FusionPresenter"; /** @internal Human-readable domain name (for debugging) */ readonly name: string; private _schema?; private _rules; private _itemUiBlocks?; private _collectionUiBlocks?; private _suggestActions?; private _agentLimit?; private _embeds; private _sealed; private _compiledStringify; private _compiledRedactor; private _redactConfig; private _redactPaths; /** @internal Use {@link createPresenter} factory instead */ constructor(name: string); /** * Ensure the Presenter is not sealed. * Throws a clear error if `.make()` has already been called. * @internal */ private _assertNotSealed; /** * Set the Zod schema for data validation and filtering. * * The schema acts as a **security contract**: only fields declared * in the schema will reach the LLM. Sensitive data (passwords, * tenant IDs, internal flags) is automatically stripped. * * @typeParam TSchema - Zod type with output type inference * @param zodSchema - A Zod schema (z.object, z.array, etc.) * @returns A narrowed Presenter with the schema's output type * * @example * ```typescript * createPresenter('Invoice') * .schema(z.object({ * id: z.string(), * amount_cents: z.number(), * })) * // Presenter<{ id: string; amount_cents: number }> * ``` */ schema>(zodSchema: TSchema): Presenter; /** * Set the schema from an object of ZodTypes (enables `t.*` shorthand). * * Accepts a plain object where each value is a ZodType. * Internally wraps it into `z.object(shape)` for full validation. * * @param shape - Object shape with ZodType values (e.g. `{ id: t.string, name: t.string }`) * @returns A narrowed Presenter with the inferred output type * * @example * ```typescript * import { createPresenter, t } from '@vinkius-core/mcp-fusion'; * * createPresenter('Invoice') * .schema({ * id: t.string, * amount: t.number.describe('in cents'), * status: t.enum('draft', 'paid'), * }) * ``` */ schema(shape: TShape): Presenter>>; /** * Set JIT system rules that travel with the data. * * Rules are **Context Tree-Shaking**: they only appear in the LLM's * context when this specific domain's data is returned. * * Accepts either a **static array** or a **dynamic function** that * receives the data and request context for RBAC/DLP/locale rules. * Return `null` from dynamic rules to conditionally exclude them. * * @param rules - Array of rule strings, or a function `(data, ctx?) => (string | null)[]` * @returns `this` for chaining * * @example * ```typescript * // Static rules * .systemRules(['CRITICAL: amounts in CENTS.']) * * // Dynamic context-aware rules (RBAC) * .systemRules((user, ctx) => [ * ctx?.user?.role !== 'admin' ? 'Mask email and phone.' : null, * `Format dates using ${ctx?.tenant?.locale ?? 'en-US'}` * ]) * ``` */ systemRules(rules: readonly string[] | ((data: T, ctx?: unknown) => (string | null)[])): this; /** * Define UI blocks generated for a **single data item**. * * The callback receives the validated data item and optionally the * request context. Return `null` for conditional blocks (filtered * automatically). * * @param fn - `(item, ctx?) => (UiBlock | null)[]` * @returns `this` for chaining * * @example * ```typescript * .uiBlocks((invoice, ctx) => [ * ui.echarts({ series: [...] }), * ctx?.user?.department === 'finance' ? ui.echarts(advancedChart) : null, * ]) * ``` */ uiBlocks(fn: ItemUiBlocksFn): this; /** * Define aggregated UI blocks for a **collection** (array) of items. * * When the handler returns an array, this callback is called **once** * with the entire validated array. Prevents N individual charts * from flooding the LLM's context. * * @param fn - `(items[], ctx?) => (UiBlock | null)[]` * @returns `this` for chaining * * @example * ```typescript * .collectionUiBlocks((invoices) => [ * ui.echarts({ xAxis: { data: invoices.map(i => i.id) } }), * ui.summary(`${invoices.length} invoices found.`), * ]) * ``` */ collectionUiBlocks(fn: CollectionUiBlocksFn): this; /** * Set a cognitive guardrail that truncates large collections. * * Protects against context DDoS: if a tool returns thousands of * records, the Presenter automatically truncates the data array * and injects a summary block teaching the AI to use filters. * * @param max - Maximum items to keep in the data array * @param onTruncate - Callback that generates a warning UI block. * Receives the count of omitted items. * @returns `this` for chaining * * @example * ```typescript * .agentLimit(50, (omitted) => * ui.summary(`⚠️ Truncated. 50 shown, ${omitted} hidden. Use filters.`) * ) * ``` */ agentLimit(max: number, onTruncate: (omittedCount: number) => UiBlock): this; /** * Define HATEOAS-style action suggestions based on data state. * * The callback inspects the data and returns suggested next tools, * guiding the AI through the business state machine. Eliminates * routing hallucinations by providing explicit next-step hints. * * @param fn - `(data, ctx?) => ActionSuggestion[]` * @returns `this` for chaining * * @example * ```typescript * .suggestActions((invoice, ctx) => { * if (invoice.status === 'pending') { * return [ * { tool: 'billing.pay', reason: 'Offer immediate payment' }, * { tool: 'billing.send_reminder', reason: 'Send reminder email' }, * ]; * } * return []; * }) * ``` */ suggestActions(fn: SuggestActionsFn): this; /** * Alias for `.suggestActions()` — fluent shorthand. * * Define HATEOAS-style action suggestions based on data state. * Use with the `suggest()` helper for maximum fluency. * * @param fn - `(data, ctx?) => (ActionSuggestion | null)[]` * @returns `this` for chaining * * @example * ```typescript * import { suggest } from '@vinkius-core/mcp-fusion'; * * .suggest((inv) => [ * suggest('invoices.get', 'View details'), * inv.status === 'overdue' * ? suggest('billing.remind', 'Send reminder') * : null, * ]) * ``` */ suggest(fn: SuggestActionsFn): this; /** * Alias for `.systemRules()` — fluent shorthand. * * @param rules - Static rules array or dynamic `(data, ctx?) => (string | null)[]` * @returns `this` for chaining * * @example * ```typescript * .rules(['CRITICAL: amounts in CENTS.']) * .rules((inv) => [ * inv.status === 'overdue' ? '⚠️ OVERDUE' : null, * ]) * ``` */ rules(rules: readonly string[] | ((data: T, ctx?: unknown) => (string | null)[])): this; /** * Alias for `.uiBlocks()` — fluent shorthand. * * @param fn - `(item, ctx?) => (UiBlock | null)[]` * @returns `this` for chaining * * @example * ```typescript * .ui((inv) => [ * ui.echarts({ series: [{ type: 'gauge', data: [{ value: inv.amount / 100 }] }] }), * ]) * ``` */ ui(fn: ItemUiBlocksFn): this; /** * Cognitive guardrail shorthand with auto-generated message. * * Truncates large collections and injects a smart summary block. * For custom truncation messages, use `.agentLimit(max, onTruncate)` instead. * * @param max - Maximum items to keep in the data array * @returns `this` for chaining * * @example * ```typescript * // Auto-generated message: * .limit(50) * // → "⚠️ Dataset truncated. 50 shown, {omitted} hidden. Use filters to narrow results." * * // For custom message, use agentLimit(): * .agentLimit(50, (omitted) => ui.summary(`Custom: ${omitted} hidden`)) * ``` */ limit(max: number): this; /** * Compose a child Presenter for a nested relation. * * When `data[key]` exists, the child Presenter's rules and UI blocks * are merged into the parent response. This is the DRY solution for * relational data: define `ClientPresenter` once, embed it everywhere. * * @param key - The property key containing the nested data * @param childPresenter - The Presenter to apply to `data[key]` * @returns `this` for chaining * * @example * ```typescript * import { ClientPresenter } from './ClientPresenter'; * * export const InvoicePresenter = createPresenter('Invoice') * .schema(invoiceSchema) * .embed('client', ClientPresenter); * ``` */ embed(key: string, childPresenter: Presenter): this; /** * Declare PII fields to redact before data leaves the framework. * * Uses `fast-redact` (Pino's V8-optimized serialization engine) to * compile object paths into hyper-fast masking functions at config * time — zero overhead on the hot path. * * The redaction is applied **structurally** on the wire-facing data * (after `_select` filter, before `ResponseBuilder`). UI blocks and * system rules still see the **full unmasked data** (Late Guillotine * pattern preserved). * * Requires `fast-redact` as an optional peer dependency. * If not installed, passes data through unmodified (defensive fallback). * * @param paths - Object paths to redact. Supports dot notation, * bracket notation, wildcards (`'*'`), and array indices. * @param censor - Replacement value. Default: `'[REDACTED]'`. * Can be a function `(originalValue) => maskedValue`. * @returns `this` for chaining * * @example * ```typescript * // Basic PII masking * createPresenter('Patient') * .schema({ name: t.string, ssn: t.string, diagnosis: t.string }) * .redactPII(['ssn', 'diagnosis']) * * // Wildcard — redact all nested SSN fields * createPresenter('Users') * .redactPII(['*.ssn', '*.password', 'credit_card.number']) * * // Array wildcard — redact diagnosis in all patients * createPresenter('Hospital') * .redactPII(['patients[*].diagnosis', 'patients[*].ssn']) * * // Custom censor — last 4 digits visible * createPresenter('Payment') * .redactPII(['credit_card.number'], (v) => '****-****-****-' + String(v).slice(-4)) * ``` * * @see {@link https://github.com/davidmarkclements/fast-redact | fast-redact} */ redactPII(paths: string[], censor?: string | ((value: unknown) => string)): this; /** * Alias for `.redactPII()` — fluent shorthand. * * @param paths - Object paths to redact * @param censor - Replacement value or function * @returns `this` for chaining * * @example * ```typescript * createPresenter('User') * .schema({ name: t.string, ssn: t.string }) * .redact(['ssn']) * ``` */ redact(paths: string[], censor?: string | ((value: unknown) => string)): this; /** * Get the Zod schema's top-level keys. * * Returns the field names declared in the Presenter's schema. * Safe to call at any time — does NOT seal the Presenter. * * @returns Array of key names, or empty array if no schema is set */ getSchemaKeys(): string[]; /** * Get the DLP redaction paths configured on this Presenter. * * Returns the paths passed to `.redactPII()` / `.redact()`. * Empty array if no redaction is configured. * * @internal Used by PostProcessor for `dlp.redact` telemetry */ getRedactPaths(): readonly string[]; /** * Get the agent limit config (if set via `.agentLimit()` or `.limit()`). * * @internal Used by PostProcessor for guardrail telemetry * @returns The max limit, or `undefined` if no limit is configured */ getAgentLimitMax(): number | undefined; /** * Get which UI block factory methods were configured. * * Inspects the configuration callbacks to determine supported * UI block types. Does NOT execute any callbacks. * * @returns Array of UI block type labels */ getUiBlockTypes(): string[]; /** * Whether the Presenter uses dynamic (context-aware) system rules. * * Static rules (string arrays) are NOT contextual. * Functions `(data, ctx?) => ...` ARE contextual. * * @returns `true` if rules are a function */ hasContextualRules(): boolean; /** * Return static rule strings for introspection hashing. * * If rules are dynamic (function), returns an empty array because * the actual rule content depends on runtime data/context. * * @returns Static rule strings, or empty array if rules are contextual */ getStaticRuleStrings(): readonly string[]; /** * Compose a {@link ResponseBuilder} from raw data. * * Orchestrates: truncate → validate → embed → render UI → attach rules * → suggest actions → **Late Guillotine** (`_select` filter). * * **Late Guillotine pattern**: UI blocks, system rules, and action * suggestions are computed using the **full** validated data, ensuring * charts and rules never see `undefined` for pruned fields. Only the * wire-facing data block in the ResponseBuilder is filtered by `_select` * — the UI consumes full data in RAM, the AI consumes pruned data on * the wire. * * After the first call, the Presenter is sealed (immutable). * * **Auto-detection**: If `data` is an array, items are validated * individually and `collectionUiBlocks` is called (if defined). * Otherwise, `uiBlocks` is called for the single item. * * @param data - Raw data from the handler (object or array) * @param ctx - Optional request context (for RBAC, locale, etc.) * @param selectFields - Optional top-level field names to keep in the * data block. When provided, only these keys survive in the JSON * payload sent to the AI. Nested objects are kept whole (shallow). * @returns A {@link ResponseBuilder} ready for chaining or `.build()` * @throws If Zod validation fails * * @example * ```typescript * // Full data (default) * return InvoicePresenter.make(rawInvoice).build(); * * // With _select filtering — only 'status' reaches the AI * return InvoicePresenter.make(rawInvoice, ctx, ['status']).build(); * ``` */ make(data: T | T[], ctx?: unknown, selectFields?: string[]): ResponseBuilder; /** * Apply PII redaction to wire-facing data. * Creates a deep clone to preserve original data for UI/rules. * @internal */ private _applyRedaction; /** * Validate data through the Zod schema (if configured). * For arrays, each item is validated independently. * @internal */ private _validate; /** * Generate and attach UI blocks to the response builder. * Auto-detects single vs collection. Filters `null` blocks. * @internal */ private _attachUiBlocks; /** * Resolve and attach domain rules to the response builder. * Supports both static arrays and dynamic context-aware functions. * Filters `null` rules automatically. * @internal */ private _attachRules; /** * Evaluate and attach action suggestions to the response. * Generates a `[SYSTEM HINT]` block with recommended next tools. * @internal */ private _attachSuggestions; /** * Process embedded child Presenters for nested relational data. * Merges child UI blocks and rules into the parent builder. * @internal */ private _processEmbeds; } /** * Create a new domain-level Presenter. * * The Presenter defines how a specific domain model (Invoice, Task, * Project) is presented to AI agents. It is **reusable** across any * tool that returns that model. * * @param name - Human-readable domain name (for debugging/logging) * @returns A new {@link Presenter} ready for configuration * * @example * ```typescript * import { createPresenter, ui } from '@vinkius-core/mcp-fusion'; * * export const TaskPresenter = createPresenter('Task') * .schema(taskSchema) * .systemRules(['Use emojis: 🔄 In Progress, ✅ Done']) * .uiBlocks((task) => [ui.markdown(`**${task.title}**: ${task.status}`)]); * ``` * * @see {@link Presenter} for the full API */ export declare function createPresenter(name: string): Presenter; export {}; //# sourceMappingURL=Presenter.d.ts.map