import { AIInterface, AIMessage, AITool, ChatOptions } from '@happyvertical/ai'; import { PrincipalAuditSink, PrincipalBinding, PrincipalRun, PrincipalTool } from '@happyvertical/smrt-agents'; import { SmrtClassOptions } from '@happyvertical/smrt-core'; import { PermissionDefinition } from '@happyvertical/smrt-users'; /** Default ceiling on tool-executing rounds before the loop force-terminates. */ export declare const DEFAULT_MAX_STEPS = 8; /** * A single manifest operation the loop can offer and execute. Its {@link slug} * is simultaneously the tool's stable name AND its permission-catalog slug — one * source of truth for both what the model may call and what the principal must * be permitted to do. */ export interface ManifestTool { /** Catalog slug (`collection.action`) — the tool name and the permission slug. */ slug: string; /** Collection (permission resource), e.g. `articles`. */ collection: string; /** Registry class name used to resolve the backing collection, e.g. `Article`. */ className: string; /** Catalog action: `read` / `create` / `update` / `delete`, or a public custom method name. */ action: string; /** Qualified class name, when known. */ qualifiedName?: string; /** Human-readable description surfaced to the model. */ description?: string; } /** * The record of one tool invocation attempt in a loop turn. */ export interface ToolInvocation { /** The tool name the model asked for. */ slug: string; /** Parsed arguments (best-effort JSON parse of the model's raw arguments). */ args: Record; /** Whether the operation executed successfully. */ ok: boolean; /** The JSON-serializable observation fed back to the model. */ observation: unknown; /** True when the call was denied (not on the allow-list / not permitted). */ rejected: boolean; /** Error summary when `ok` is false. */ error?: string; } /** Why {@link runToolLoop} returned. */ export type ToolLoopStopReason = 'stop' | 'max_steps' | 'no_tools'; /** The outcome of a {@link runToolLoop} turn. */ export interface ToolLoopResult { /** The model's final assistant text. */ content: string; /** Number of tool-executing rounds completed. */ steps: number; /** Why the loop stopped. */ stoppedReason: ToolLoopStopReason; /** Every tool invocation attempted this turn, in order. */ invocations: ToolInvocation[]; /** The full working transcript (input messages + assistant/tool turns). */ messages: AIMessage[]; /** Total tokens reported by the AI boundary, when available. */ totalTokens: number; } /** Context handed to a custom {@link ToolLoopOptions.executeTool} implementation. */ export interface ToolExecutionContext { /** The principal run whose context bounds this execution. */ run: PrincipalRun; /** The manifest operation to execute. */ tool: ManifestTool; /** Parsed tool arguments. */ args: Record; /** The database handle to operate against (already the RLS-bound tx when on). */ db?: SmrtClassOptions['db']; } /** * Options for {@link runToolLoop}. */ export interface ToolLoopOptions { /** The AI boundary (the only thing mocked in tests). */ ai: AIInterface; /** The initial conversation messages (system / history / user). */ messages: AIMessage[]; /** The manifest operations available this turn (already allow-list-filtered). */ tools: ManifestTool[]; /** * Non-manifest tools offered alongside the manifest operations — e.g. the * agent-orchestration `invoke-agent` tool (#1892). Each is gated by the same * fail-closed allow-list: only pass a tool whose `slug` is on the persona's * `allowedTools`, and its `execute` re-asserts the gate. Offered to the model * with its own `aiTool` definition and routed to its own handler. */ extraTools?: PrincipalTool[]; /** The persona principal every tool call runs as. */ principal: PrincipalBinding; /** Database handle the side-door operations run against. */ db?: SmrtClassOptions['db']; /** Max tool-executing rounds before force-termination. Default {@link DEFAULT_MAX_STEPS}. */ maxSteps?: number; /** Model id passed to the AI boundary. */ model?: string; /** Sampling temperature. */ temperature?: number; /** Max tokens per completion. */ maxTokens?: number; /** Tool-choice behaviour while tools are offered. Default `'auto'`. */ toolChoice?: ChatOptions['toolChoice']; /** * Override the side-door executor. The default * ({@link invokeManifestTool}) enforces the allow-list + catalog gate and * dispatches through the ObjectRegistry. Tests inject a stub to exercise loop * mechanics without a backing object. */ executeTool?: (ctx: ToolExecutionContext) => Promise; /** Notified after each tool invocation (for streaming/telemetry). */ onInvocation?: (invocation: ToolInvocation) => void | Promise; /** * Token sink for live streaming (#1936). When set, each `ai.chat` round is run * with `stream: true` and the model's text deltas are forwarded here as they * arrive. It is best-effort: a provider that cannot stream (or streams no text * on a tool-call round) simply never calls it, and the fully-resolved response * is still returned. Deltas across ALL rounds are forwarded — a tool-call * round may narrate before calling a tool — so the emitted tokens are a live * PREVIEW; the loop's final `content` (persisted + surfaced by the caller as * the authoritative message) is the source of truth. */ onToken?: (chunk: string) => void; /** The originating user the turn runs on behalf of (audited). */ onBehalfOfUserId?: string | null; /** Canonical agent class, recorded in the audit entry. */ agentClass?: string; /** Audit sink forwarded to {@link executeAsPrincipal}. */ audit?: PrincipalAuditSink; /** Opt into Postgres RLS transaction wrapping. */ postgresRls?: boolean; } /** * Build the closed catalog of manifest operations available as tools. * * Reads the manifest-derived {@link PermissionCatalog} and keeps only the * entries that name a dispatchable operation (a `(collection, action)` with a * resolvable backing class). Pass `allowedTools` to narrow the catalog to a * persona's least-privilege allow-list — this is the **offer gate**: a slug not * in `allowedTools` is never returned, so it is neither offered to the model nor * executed. A missing, `null`, or empty `allowedTools` yields **no tools** * (fail-closed) — the same whitelist semantics as `AgentSession`/ * `PrincipalBinding` (S5 #1392), so forgetting the allow-list can only tighten, * never widen, the offered surface. Pass `all: true` to deliberately enumerate * the full manifest operation surface (e.g. an admin tool picker) — that is the * one explicit escape hatch, never the default. */ export declare function buildManifestToolCatalog(options?: SmrtClassOptions & { /** Least-privilege allow-list to narrow the catalog by (fail-closed). */ allowedTools?: string[] | null; /** Explicitly enumerate the ENTIRE manifest operation surface (no narrowing). */ all?: boolean; /** Supply a pre-built catalog (skips the manifest walk). */ catalog?: PermissionDefinition[]; }): ManifestTool[]; /** * A provider-safe function name for a catalog slug. * * Catalog slugs are `collection.action` and routinely contain a `.`, but many * providers (OpenAI) restrict function names to `[A-Za-z0-9_-]{1,64}`. This maps * the slug into that charset (dots → `-`) for the wire; {@link runToolLoop} maps * the returned name back to the tool, and `tool.slug` remains the internal * permission id. Distinct slugs stay distinct (the only substituted char is the * single `.` separator). */ export declare function toolFunctionName(slug: string): string; /** * Project a manifest operation into an AI function-tool definition. The function * name is the provider-safe rendering of the catalog slug * ({@link toolFunctionName}), so the model can only ever name a real operation. */ export declare function manifestToolToAITool(tool: ManifestTool): AITool; /** * Execute a manifest operation in-process ("side door") under the principal. * * Enforces both authority dimensions before touching data: the fail-closed tool * allow-list ({@link PrincipalRun.assertToolAllowed}) and the catalog permission * for the `(collection, action)` ({@link PrincipalRun.assertOperation}) — the * door-agnostic teeth that hold on RLS-off adapters and are a redundant second * gate under Postgres RLS. Data operations run against the principal context's * database (the RLS-bound transaction when RLS is on), so tenant + per-operation * enforcement apply exactly as they would through REST or MCP. */ export declare function invokeManifestTool(run: PrincipalRun, tool: ManifestTool, args: Record, options?: { db?: SmrtClassOptions['db']; }): Promise; /** * Run a bounded `tool_call → observe → respond` loop over the manifest operation * surface, as the persona's bound principal. * * The whole turn runs inside a single {@link executeAsPrincipal} context, so * every tool call shares one published permission snapshot (matching what a * Postgres RLS session enforces) and the turn audits once as on-behalf-of the * originating user. * * @param options - The AI boundary, seed messages, allow-list-filtered tools, * principal, and ceiling. * @returns The final assistant text plus the invocation log and transcript. */ export declare function runToolLoop(options: ToolLoopOptions): Promise; //# sourceMappingURL=tool-loop.d.ts.map