UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

232 lines • 12.4 kB
/** * Asking the USER a structured question, and waiting for the answer. * * `awaitApproval` collects a yes/no about work already proposed; this collects the scope BEFORE the * work. Two surfaces produce it — a configured intake (`AgentLoopDeps.intake`) and the model-callable * `ask` tool (`AgentLoopDeps.ask`) — and they deliberately produce the SAME {@link * ElicitationRequest}, persist through the same tool-call row, and resume through the same * `tool:<runId>:<callId>` signal. A consumer cannot tell which one asked, and should not have to. */ import type { StandardSchemaV1 } from '@standard-schema/spec'; import { type ElicitationInput } from './elicitation-input.js'; import type { Decision, ToolDefinition } from './types.js'; /** * Whatever a human sent into a parked run. One channel carries both, because a question set is * persisted as a `pending_approval` action so it lands in the approvals inbox a deployment already * has — which means an operator pressing Approve there delivers a {@link Decision} to a run that is * waiting on answers. The loop reduces one to the other; see {@link normalizeElicitationReply}. */ export type HumanReply = Decision | ElicitationReply; /** * Thrown when a reply is the wrong shape for the wait it was delivered to: answers to a question * set, addressed to a tool call that is waiting on an approve/reject. * * Refused rather than reduced, because there is no honest reduction in that direction. A yes/no can * be read as "confirmed the pre-picked answers" (see {@link normalizeElicitationReply}); a set of * answers says nothing at all about whether the proposed work should go ahead, and reading its * absent `approved` as `false` would record a rejection the person never made. */ export declare class HumanReplyMismatchError extends Error { readonly runId: string; readonly toolCallId: string; constructor(runId: string, toolCallId: string); } /** * Is this a human's yes/no {@link Decision}, rather than answers to a question set? Structural * rather than by class: both shapes are plain JSON off an HTTP body by the time they arrive, and * `approved` is the only field a `Decision` carries. */ export declare function isHumanDecision(reply: HumanReply): reply is Decision; /** One choice a question offers. */ export interface ElicitationOption { /** Stable identifier submitted back. Never shown to the user. */ value: string; /** What the user reads. */ label: string; /** * A single character a UI may bind as a keyboard shortcut for this option. Advisory — nothing in * the library reads it, and a client is free to render its own. */ hotkey?: string; } /** One question in a set. */ export interface ElicitationQuestion { /** Unique within its request; the key answers come back under. */ id: string; prompt: string; /** A line of help under the prompt. */ description?: string; /** * The choices. Required unless {@link input} asks for a typed value — then optional, and a UI may * offer them as suggestions. An `input` of type `select` still picks from these. */ options?: ElicitationOption[]; /** * Ask for a typed value (a number, a date, a sentence…) instead of — or, for `select`, on top of — * a pick from `options`. The answer is still a `string[]`, in the type's canonical form; see * `elicitation-input.ts`. */ input?: ElicitationInput; /** More than one option may be chosen. Omit → single choice. */ multiple?: boolean; /** * The options already picked for the user. The claim this whole surface makes is that confirming * is enough, so a question with no defaults is a question the user must stop and think about — * which is the case the design is trying to avoid. Empty/omitted is allowed and means exactly * that: submitting without answering leaves this question unanswered. */ defaults?: string[]; /** Accept values that are not among `options` (a typed-in answer). Omit → options only. */ allowFreeText?: boolean; } /** * A question set awaiting a human. Identical in shape whether an `@Agent`'s configured intake or * the model's `ask` tool authored it — `source` records which, for audit, not for control flow. * * `questions.length` is known when the request is written, which is what lets a client render * "Question 1 of 3" without guessing whether a fourth is coming. */ export interface ElicitationRequest { /** The tool-call id this request is persisted under, and the signal it is answered through. */ id: string; source: 'intake' | 'ask'; /** What the assistant says above the form. */ preamble?: string; questions: ElicitationQuestion[]; } /** What a human sent back for an {@link ElicitationRequest}. */ export interface ElicitationReply { /** * questionId → chosen values. A question whose id is ABSENT takes the request's own `defaults` — * that is what makes "just submit" mean "yes, your pre-picked answers". A present-but-empty array * is an explicit "none of these" and does NOT fall back. */ answers: Record<string, string[]>; /** * The user declined to answer and told the agent to proceed on its own assumptions. Distinct from * confirming the defaults even though the resulting values are the same: one is a decision the * user made, the other is one they refused to make, and only the first is evidence of intent. */ skipped?: boolean; /** Opaque ref of WHO answered, when it wasn't the run's own actor. */ answeredByRef?: string; /** * The surface the answer came through — `'web'`, `'slack'`, `'console'`, … — the counterpart of * an approval's `decidedVia`. */ answeredVia?: string; } /** A settled elicitation: what the agent proceeds on, and how it got there. */ export interface ElicitationOutcome { /** One entry per question, in request order — always present, so a caller never re-applies defaults. */ answers: Record<string, string[]>; skipped: boolean; /** Question ids filled from the request's `defaults` rather than by the human. */ defaulted: string[]; /** * Who answered (or skipped) — the reply's `answeredByRef` (the answering actor's id). The * counterpart of an approval's `decidedBy`. Absent when the reply did not say. */ answeredBy?: string; /** The surface it came through (`'web'`, `'slack'`, …) — an approval's `decidedVia`. */ answeredVia?: string; } /** * Read whatever the human channel delivered as an {@link ElicitationReply}. * * A question set is persisted as an `action` tool call in `pending_approval` — that is what puts it * in the approvals inbox a deployment already has, instead of needing one of its own. The cost of * that choice is that the thing which comes back may be a {@link Decision} someone pressed * Approve/Reject on rather than a set of answers, and a `Decision` carries no `answers` at all. * * Approve means every question keeps its own pre-picked `defaults`, which is exactly what "just * submit" already means on this surface; Reject is the same declining-to-answer a skip is. Neither * reading is a guess — a yes/no channel cannot say more than that, and saying it here is what lets * one inbox settle both kinds of pending work. * * Returns the reply UNCHANGED when it already carries answers, so the common path allocates nothing * and a caller can identity-compare. */ export declare function normalizeElicitationReply(reply: HumanReply): ElicitationReply; /** * Settle a reply against the request it answers: fill every unanswered question from its own * `defaults`, drop submitted values that aren't on offer, and collapse a single-choice question to * one value. * * PURE, and deliberately so. Both of its inputs are already journaled by the time the loop calls it * — the request came from module config or from an `llm:<i>` checkpoint, the reply from the signal * checkpoint — so every process replaying the turn reaches the same values without a checkpoint of * its own. Resolving defaults in the HTTP layer instead would put them behind a store read that a * replay would have to repeat. */ export declare function resolveElicitation(args: { request: ElicitationRequest; reply: HumanReply; }): ElicitationOutcome; /** * What a settled elicitation looks like to everyone downstream: the model reading it back as a tool * result, the thread reader rendering it, the auditor asking what the agent was told to do. One * shape for both surfaces — nothing here records which of them asked. */ export interface ElicitationResult extends ElicitationOutcome { /** The questions against the chosen LABELS, so a reader (and a model) can act on it. */ summary: string; } /** {@link resolveElicitation} plus its human-readable rendering. Pure, for the same reason. */ export declare function settleElicitation(args: { request: ElicitationRequest; reply: HumanReply; }): ElicitationResult; /** * The answers as the model reads them: the question's own prompt against the chosen options' LABELS, * not their opaque `value`s — a model shown `{"scope":["b"]}` has been told nothing. */ export declare function renderElicitationAnswers(args: { request: ElicitationRequest; outcome: ElicitationOutcome; }): string; /** The reserved tool name the model calls to ask the user something. */ export declare const ASK_TOOL_NAME = "ask"; /** What the model must supply when it calls `ask`. */ export interface AskToolInput { preamble?: string; questions: ElicitationQuestion[]; } /** How many questions one `ask` may carry. A form the user has to scroll is a form they skip. */ export declare const MAX_ASK_QUESTIONS = 5; /** * The `ask` tool's input schema, hand-written rather than borrowed from a validation library: core * depends on no validator, and the schema has to carry a JSON Schema a provider can constrain * generation against. It publishes one through the Standard JSON Schema extension * (`~standard.jsonSchema.input`), which is the path the AI SDK adapter already recognises for * Valibot / ArkType / Zod 4. */ export declare const askInputSchema: StandardSchemaV1<unknown, AskToolInput>; /** * What the model is told the `ask` tool is for. Written to discourage the two failure modes that * make a clarifying question worse than a guess: asking about something the conversation already * settled, and asking without saying what you would have done. */ export declare const ASK_TOOL_DESCRIPTION = "Ask the user to settle the scope of the work before you do it. Use it when a reasonable person would produce a materially different result depending on the answer \u2014 not to confirm something the conversation already says. Every question must pre-pick the answer you would choose, so the user can confirm instead of deciding. The user may decline, in which case you proceed on those pre-picked answers."; /** * The `ask` tool as the model sees it. NOT a `ToolSpec` and never registered: `ask` has no handler, * because the loop settles it against a human instead of invoking anything. Keeping it out of the * `ToolRegistry` is also what keeps the kind decision off a process-local lookup — see * `claimToolCall`. */ export declare function askToolDefinition(): ToolDefinition; /** A question set an `@Agent` asks before it starts working. See `AgentLoopDeps.intake`. */ export interface AgentIntake { questions: ElicitationQuestion[]; /** What the assistant says above the form. Omit → {@link DEFAULT_INTAKE_PREAMBLE}. */ preamble?: string; /** * `'thread-start'` (default) asks once, on the first turn of a thread; `'every-turn'` asks before * every turn. Both are decided from what `load:thread` recorded about the thread when the turn * began, never from anything this process happens to know — by the time a replay reaches the * question, the thread already holds the assistant message the first attempt wrote. */ when?: 'thread-start' | 'every-turn'; } export declare const DEFAULT_INTAKE_PREAMBLE = "A few questions before I start. I have pre-picked what I would choose, so confirming is enough."; //# sourceMappingURL=elicitation.d.ts.map