@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
139 lines • 7.27 kB
TypeScript
import type { StandardSchemaV1 } from '@standard-schema/spec';
import { type BrandedFunctionalTool, type DefineToolOptions } from './ai-tool-ref.js';
import type { ConfirmTokenStore } from './spi/confirm-token-store.js';
import type { AiToolCtx } from './spi/tool.js';
/** The two fields {@link defineConfirmedTool} adds to a tool's input. */
export interface ConfirmFields {
/** `true` to commit; absent (or `false`) to preview. */
confirm?: boolean;
/** The `confirmToken` the preview returned. */
confirmToken?: string;
}
/** The words a confirmed tool says to the model. Each has an English default. */
export interface ConfirmedToolMessages {
/** Sent with every preview: how to confirm. `{minutes}` is replaced with the token's lifetime. */
confirm?: string;
/** Thrown for a missing, tampered, mismatched or expired token. */
invalid?: string;
/** Thrown for a token that already committed. */
used?: string;
}
/** Thrown by a confirmed tool that refused a confirmation. Nothing was written. */
export declare class ConfirmTokenError extends Error {
/** `invalid`: missing, tampered, mismatched or expired. `used`: it already committed once. */
readonly reason: 'invalid' | 'used';
constructor(
/** `invalid`: missing, tampered, mismatched or expired. `used`: it already committed once. */
reason: 'invalid' | 'used', message: string);
}
/** What {@link defineConfirmedTool} takes besides the usual tool options. */
export interface ConfirmedToolOptions extends Omit<DefineToolOptions, 'kind'> {
name: string;
/**
* The HMAC key the tokens are signed with — the app key, or a secret of its own. A function is
* read on every call. Required: there is no default, and an empty one throws.
*/
secret: string | (() => string);
/** How long a preview stays confirmable. Default 15 minutes. */
ttlMs?: number;
/**
* Makes a token single use. WITHOUT a store a token commits as many times as it is sent until it
* expires — a retry or a repeated call writes twice.
*/
store?: ConfirmTokenStore;
messages?: ConfirmedToolMessages;
}
/** What a step reports: a sentence for the model to relay, and the structured detail behind it. */
export interface ConfirmedToolOutcome {
summary: string;
data?: unknown;
}
/** The three steps of a confirmed write. */
export interface ConfirmedToolSteps<Args, Prepared> {
/**
* Resolve and validate everything the write needs, applying the SAME rules the commit relies on.
* Throw to refuse. Writes nothing. Runs for the preview AND again for the confirmation — state may
* have changed in between — and a refusal here does not spend the token.
*/
prepare(args: Args, ctx: AiToolCtx): Promise<Prepared> | Prepared;
/** What is about to happen. Writes nothing. */
preview(prepared: Prepared, ctx: AiToolCtx): Promise<ConfirmedToolOutcome> | ConfirmedToolOutcome;
/** Write, and say what was written. A throw gives the token back. */
commit(prepared: Prepared, ctx: AiToolCtx): Promise<ConfirmedToolOutcome> | ConfirmedToolOutcome;
}
/** A confirmed tool's answer to a call without `confirm`. */
export interface ConfirmedToolPreview {
status: 'preview';
summary: string;
data?: unknown;
confirmToken: string;
/** ISO-8601 instant the token stops working. */
expiresAt: string;
/** Instructions for the model: show the preview, then call again with the token. */
confirm: string;
}
/** A confirmed tool's answer to a confirmation that committed. */
export interface ConfirmedToolDone {
status: 'done';
summary: string;
data?: unknown;
}
export type ConfirmedToolResult = ConfirmedToolPreview | ConfirmedToolDone;
/** The JSON Schema of {@link ConfirmFields}, merged into the schema the model is shown. */
export declare const CONFIRM_JSON_SCHEMA_PROPERTIES: {
readonly confirm: {
readonly type: 'boolean';
readonly description: 'Omit to get a preview (nothing is written). Send true, with confirmToken, to commit.';
};
readonly confirmToken: {
readonly type: 'string';
readonly description: 'The confirmToken the preview returned. Required when confirm is true.';
};
};
/**
* Wrap a tool's input schema so it also accepts {@link ConfirmFields}, without the app declaring
* them. `validate` takes the two fields off, checks them itself, hands the REST to the app's schema
* (so a strict schema never sees a key it did not declare) and puts them back on the validated value.
*
* The wrapper always carries the Standard JSON Schema extension, because that is the only form the
* model gets to see: the app schema's own JSON Schema plus the two properties. A schema without the
* extension (Zod 3, a bare Standard Schema) degrades to a permissive object that still declares the
* two fields — `validate` stays the authority either way.
*/
export declare function withConfirmFields<Schema extends StandardSchemaV1>(schema: Schema): StandardSchemaV1<StandardSchemaV1.InferInput<Schema> & ConfirmFields, StandardSchemaV1.InferOutput<Schema> & ConfirmFields>;
/**
* A write with a human gate that lives INSIDE the tool: preview first, commit on confirmation.
*
* - Called without `confirm`: runs `prepare`, writes nothing, and returns the `preview` with a signed
* `confirmToken`.
* - Called again with the SAME arguments, `confirm: true` and that token: runs `prepare` again, then
* `commit`.
*
* The token is an HMAC over the tool, `ctx.actor.id`, `ctx.actor.tenantRef`, the expiry and the
* canonical arguments, so it cannot be replayed by another actor, in another tenant, on another
* tool or with a changed argument. With a `store` it is also single use: it is claimed right before
* `commit` (a refusal in `prepare` does not spend it) and released if `commit` throws.
*
* Registered as `kind: 'read'` on purpose. An `action` parks on the loop's HITL approval, which an
* MCP caller cannot answer — `createMcpServer` keeps `action` tools off the surface, and MCP clients
* without elicitation have no other channel. The gate being in the handler is what lets the same
* tool serve the chat loop and MCP; the user sees the preview because the model has to relay it to
* get a token it can confirm with.
*
* ```ts
* export const refundOrder = defineConfirmedTool(
* { name: 'refund_order', description: '...', input: z.object({ orderId: z.string() }),
* secret: () => env.get('APP_KEY').release(), store: new LucidConfirmTokenStore(db) },
* {
* prepare: async ({ orderId }, ctx) => loadRefundableOrder(orderId, ctx.actor),
* preview: (order) => ({ summary: `Refund ${order.total} to ${order.customer}?`, data: order }),
* commit: async (order) => ({ summary: 'Refunded.', data: await refund(order) }),
* },
* )
* ```
*
* `input` is the app's own schema: `confirm` and `confirmToken` are added by
* {@link withConfirmFields}, and `prepare` receives the arguments without them.
*/
export declare function defineConfirmedTool<Args = unknown, Prepared = unknown>(options: ConfirmedToolOptions, steps: ConfirmedToolSteps<Args, Prepared>): BrandedFunctionalTool;
//# sourceMappingURL=confirmed-tool.d.ts.map