/** * Intent confirmation in the chat. When a model needs a decision before it can * continue, the host's `ask_user_question` reaches for `ctx.userQuestions` — * a seam that admits ONE provider per context, which a composed Web app claims * for its own clients. A chat agent asking through it would wait on a surface * its human is not watching, which is why this channel used to deny the tool * outright and tell the model to ask in prose instead. * * A prose question loses the structure: the options the model weighed, which * one the human picked, and the fact that an answer is due at all. So the tool * is SHADOWED instead — an agent-scoped registration of the same name, which * the host's layered registry resolves before the global one (its registry * reserves exactly one name from shadowing, and it is not this one). The * question becomes a card with the model's own options as buttons; a click * answers it, and so does an ordinary chat reply, because typing an answer is * what a person does when none of the buttons fit. * @module dsh-lark-channel/questions */ /** Marks this plugin's question buttons apart from other card actions. */ export declare const QUESTION_ACTION = "dsh-lark-channel/question"; /** How long a question waits for its human before the tool gives up. */ export declare const QUESTION_TIMEOUT_MS: number; /** One choice the model offered. */ export interface QuestionOption { readonly label: string; readonly description?: string | undefined; } /** One question the model wants answered before it continues. */ export interface AskedQuestion { /** The model's own id, echoed back in the answer so it can pair them up. */ readonly id: string; readonly question: string; readonly header?: string | undefined; readonly options?: readonly QuestionOption[] | undefined; readonly multiSelect?: boolean | undefined; } /** One answer, in the shape the host's own tool returns. */ export interface QuestionAnswer { readonly id: string; /** Labels the human chose; empty when they typed instead or declined. */ readonly selected: string[]; /** Free text the human typed, when they did. */ readonly custom?: string; } /** Card payload carried by one option choice, or by a submitted set of them. */ export interface QuestionActionValue { readonly kind: typeof QUESTION_ACTION; /** Correlation id of the pending question, not the model's own id. */ readonly id: string; /** * Index into the question's options; -1 on the submit button of a multiple * choice, whose chosen set arrives in the action's form value instead. */ readonly option: number; } /** The index a submit button carries, having no single option of its own. */ export declare const SUBMIT_OPTION = -1; /** * Narrow one card action to a question choice. * @param value - the untrusted `action.value` from a card click. * @returns the parsed choice, or undefined when the click is not one. */ export declare function questionActionValue(value: unknown): QuestionActionValue | undefined; /** * Build the card for one question. Every model-authored string rides a * `plain_text` element: a question and its options are untrusted text, and * card markup in them must render literally rather than disguise itself as * the card's own words. * @param question - the question to ask. * @param id - correlation id carried by every option button. * @returns a Feishu card object for `send({ card })`. */ export declare function questionCard(question: AskedQuestion, id: string): object; /** * Rewrite a settled question's card, so a chat scrolled back to later shows * what was decided rather than buttons that no longer do anything. * @param question - the question that was asked. * @param outcome - how it settled. * @returns a Feishu card object for `updateCard`. */ export declare function settledQuestionCard(question: AskedQuestion, outcome: { readonly answer?: string | undefined; readonly cancelled?: boolean; }): object; /** What the store needs from the transport, so tests need no Feishu. */ export interface QuestionPorts { /** Send one question card; resolves with the message it created. */ send(chatId: string, card: object): Promise; /** Rewrite a settled question's card. */ update(messageId: string, card: object): Promise; /** Operator console line. */ report(line: string): void; } /** * The questions this channel is waiting on, and the two ways they get * answered. One conversation asks one question at a time: the tool awaits each * before sending the next, so a chat never shows two open questions whose * replies could not be told apart. */ export declare class ChatQuestions { private readonly ports; private readonly pending; private counter; constructor(ports: QuestionPorts); /** Whether this session has a question waiting for a typed answer. */ awaiting(sessionId: string): boolean; /** * Ask one question and wait for the human. * @param input - the question, and where to ask it. * @returns the answer; an aborted or timed-out question answers empty. */ ask(input: { readonly sessionId: string; readonly chatId: string; readonly question: AskedQuestion; readonly signal?: AbortSignal | undefined; readonly timeoutMs?: number | undefined; }): Promise; /** * Answer by clicking an option. * @param value - the parsed card action. * @returns whether it settled a live question. */ answerByClick(value: QuestionActionValue, chosen?: readonly string[]): object | undefined; /** * Settle a multiple choice from what its form submitted. * * The submission carries positions rather than labels, so an empty or * unreadable set is a submission that named nothing this question offered — * refused rather than answered, leaving the card live for another try. * @param id - the pending question's correlation id. * @param entry - the question awaiting an answer. * @param chosen - option indices, as the form returned them. * @returns the settled card to paint, or undefined when nothing was chosen. */ private answerBySubmission; /** * Answer with typed text — what a person does when no button fits. * @param sessionId - the conversation's session. * @param text - exactly what they wrote. * @returns whether it settled a live question. */ answerByText(sessionId: string, text: string): boolean; /** * Withdraw every question of one session — its agent is going away, so * nothing is left waiting on a card nobody will answer. * @param sessionId - the conversation's session. */ cancelSession(sessionId: string): void; /** * Settle one question exactly once and repaint its card. * @param repaint - false when the caller paints the card itself, which a * click does through its own response — the only repaint path that cannot * fail silently, since the patch API reports business errors in a body the * SDK discards rather than by rejecting. */ private finish; } /** * The shadow tool definition, declared structurally like every other host * contract here so the package keeps building against two published packages * alone — the host's `defineTool` would drag its whole runtime closure into * this repository to construct one object. * * Both schemas are therefore written in their COMPILED form: real JSON Schema * with `required` as an array on each object. `defineTool` exists to perform * exactly that conversion from a per-property spec, and a definition written in * the spec form is rejected by the registry — which is the contract that * matters, and which validates this at registration. * * The schema mirrors the host's own `ask_user_question` so a model that learned * the tool from any other surface calls this one the same way. Arguments are * normalized rather than rejected: a question with a slightly-off shape is * still worth asking, where a schema error would just fail the turn. * @param ask - asks one question and resolves with its answer. * @returns the definition to register in an agent's scope. */ export declare function shadowQuestionTool(ask: (questions: readonly AskedQuestion[], agentSessionId: string | undefined) => Promise): object; //# sourceMappingURL=questions.d.ts.map