eve
Version:
Filesystem-first framework for durable backend AI agents that run anywhere.
206 lines (205 loc) • 8.85 kB
TypeScript
import type { Prompter } from "./prompter.js";
/** One choice in a select question. */
export interface SelectOption<T> {
/** Stable handle so external answers (flags, JSON) can address rich values. */
id: string;
label: string;
value: T;
hint?: string;
/**
* A leading run of featured options forms a searchable picker's default
* viewport: with no filter typed, only they are in view, and scrolling or
* filtering reaches the rest of the list. Featured options must be sorted
* to the front. Meaningless without `search`.
*/
featured?: boolean;
}
/**
* One choice in a multi-select question. Row availability is part of the
* question, not the renderer: an interactive picker renders the reason next to
* the row, and {@link withAnswers} enforces the same semantics on external
* answers (a disabled id is refused with its reason, a locked value is part of
* every answer whether or not it was named).
*/
export interface MultiSelectOption<T> extends SelectOption<T> {
/** Visible and explained but not pickable. Mutually exclusive with `locked`. */
disabled?: boolean;
/** Why the row is disabled; shown by pickers and quoted by answer refusals. */
disabledReason?: string;
/** Always selected and cannot be toggled off. */
locked?: boolean;
/** Why the row is locked; shown by pickers (e.g. "always available"). */
lockedReason?: string;
}
/**
* A keyed question a box sends through the channel. The key is the question's
* stable identity: decorators match pre-supplied answers and requiredness
* opt-ins against it, and resolutions are announced under it.
*/
export type Question<T> = {
key: string;
message: string;
/** Found in the world (e.g. an on-disk link). Feeds the confirm/assume rungs. */
detected?: T;
/** A safe assumption (e.g. the most popular model). Used by "assume". */
recommended?: T;
/**
* Questions are skippable by default; required is the opt-in. Headless and
* assume runs auto-skip non-required questions, and only a required question
* may refuse a headless run ({@link InteractionRequired}). Set by the box
* when intrinsic, or by the flow via {@link withRequired}.
*/
required?: boolean;
/**
* Synthesized by a decorator (e.g. the confirm-detected rung): interaction
* mechanics, not a box value, so bases render it but do not announce it.
*/
internal?: boolean;
} & ({
kind: "select";
options: readonly SelectOption<T>[];
/** Offer the type-ahead filter line in interactive renderers. */
search?: boolean;
/** Placeholder for the filter line while it is empty. */
placeholder?: string;
} | {
kind: "confirm";
} | {
kind: "text";
validate?: (raw: string) => string | null;
sensitive?: boolean;
/** Presentation hint: ghost text shown while the input is empty. */
placeholder?: string;
});
/**
* A keyed multi-select question. Its answer is a set (`T[]`) while its options
* carry single values (`T`), so it travels through the paired
* {@link Asker.askMany} instead of being a {@link Question} kind: forcing it
* through `ask<T>` would make the channel lie about the answer type.
*/
export interface MultiSelectQuestion<T> {
key: string;
message: string;
options: readonly MultiSelectOption<T>[];
/** Found in the world. Feeds the confirm/assume rungs and pre-marks pickers. */
detected?: readonly T[];
/** A safe assumption. Used by "assume"; pre-marks pickers without a detection. */
recommended?: readonly T[];
/** Same opt-in as {@link Question.required}: only it may refuse a headless run. */
required?: boolean;
/** Same as {@link Question.internal}: rendered but never announced. */
internal?: boolean;
/**
* Block an empty submission in interactive pickers. Distinct from `required`,
* which is the headless-refusal opt-in: a question can refuse to be skipped
* headlessly while still accepting an empty interactive selection.
*/
requireSelection?: boolean;
/** Offer the type-ahead filter line in interactive renderers. */
search?: boolean;
/** Placeholder for the filter line while it is empty. */
placeholder?: string;
}
/** Builds a select question without spelling the discriminant at call sites. */
export declare const select: <T>(q: Omit<Extract<Question<T>, {
kind: "select";
}>, "kind">) => Question<T>;
/** Builds a confirm question without spelling the discriminant at call sites. */
export declare const confirm: (q: {
key: string;
message: string;
detected?: boolean;
recommended?: boolean;
required?: boolean;
internal?: boolean;
}) => Question<boolean>;
/** Builds a text question without spelling the discriminant at call sites. */
export declare const text: (q: {
key: string;
message: string;
detected?: string;
recommended?: string;
required?: boolean;
validate?: (raw: string) => string | null;
sensitive?: boolean;
placeholder?: string;
}) => Question<string>;
/** The capability boxes see. Pure and flow-agnostic: Prompter's successor. */
export interface Asker {
ask<T>(question: Question<T>): Promise<T>;
/**
* The multi-select channel. Paired with {@link ask} instead of being a
* question kind because the answer type (`T[]`) differs from the option type
* (`T`); every rung treats it with the same ladder semantics as `ask`.
*/
askMany<T>(question: MultiSelectQuestion<T>): Promise<T[]>;
}
/** A ladder rung: wraps an asker and resolves (or rewrites) some questions. */
export type AskerDecorator = (inner: Asker) => Asker;
/** Thrown when a skippable question is skipped, so the box can branch on it. */
export declare class SkippedSignal extends Error {
readonly key: string;
constructor(key: string);
}
/** Any question the channel can carry, for signals that quote one. */
export type AnyQuestion = Question<unknown> | MultiSelectQuestion<unknown>;
/**
* Headless refusal that keeps the whole question: an agent driver can relay
* exactly what is missing (key, message, options) instead of a bare string.
*/
export declare class InteractionRequired extends Error {
readonly question: AnyQuestion;
constructor(question: AnyQuestion);
}
/** Thrown when a pre-supplied answer fails the question's own validation. */
export declare class InvalidAnswerError extends Error {
readonly key: string;
constructor(key: string, message: string);
}
/** How a question got its value, so nothing is silently assumed. */
export type ResolutionSource = "answer" | "detected" | "assumed" | "asked" | "skipped";
/** One announced question outcome. */
export interface Resolution {
key: string;
value: unknown;
source: ResolutionSource;
}
/** Every resolution is announced with its source: nothing is silently assumed. */
export interface AskerEvents {
onResolved?: (resolution: Resolution) => void;
}
/**
* The interactive base: renders every question through the existing
* {@link Prompter}, pre-filled with detected/recommended. A user cancel
* surfaces as the prompter's own WizardCancelledError and propagates to the
* runner unchanged. There is no skip gesture on non-required questions yet:
* the current prompter has none, so it arrives with the prompter rework.
*/
export declare function interactiveAsker(prompter: Prompter, events?: AskerEvents): Asker;
/**
* The base for flag/agent stacks: auto-skips non-required questions (announced
* as "skipped"), refuses required ones structurally with
* {@link InteractionRequired}.
*/
export declare function headlessAsker(events?: AskerEvents): Asker;
/**
* Pre-answers by question key (flags, config, agent tool args), validated
* against the question they answer. Unmatched questions fall through.
*/
export declare function withAnswers(answers: Record<string, unknown>, events?: AskerEvents): AskerDecorator;
/**
* Flow-level opt-in to requiredness by key: the flow declares which questions
* it cannot proceed without, boxes stay oblivious. Composes with box-intrinsic
* `required` flags (whichever marks it, it is required).
*/
export declare function withRequired(keys: readonly string[]): AskerDecorator;
/** The two detected/recommended rungs of the ladder. */
export type AnswerPolicy = "confirm-detected" | "assume";
/**
* The detected/recommended rungs. "confirm-detected" turns a detected value
* into a one-keystroke confirm (synthesized as an internal question through
* the same channel); "assume" takes detected or recommended silently but
* announced, skips the non-required unknowable, and escalates only required
* unknowables to the inner asker.
*/
export declare function withPolicy(policy: AnswerPolicy, events?: AskerEvents): AskerDecorator;