Skip to content

Question

A question put to the person mid-conversation — choices, yes or no, a short text, or several at once — answered at the composer and kept in the transcript.

  • <aparte-elicitation> + 1 part
  • @aparte/plugin-ask-user
On this page
Open

Sometimes the next step needs an answer only the person has: which format, which audience, go or wait. The question opens in place of the composer’s input, with its choices; the person answers, and the conversation carries on.

It can come from two places. The model asks it through the ask_user tool of @aparte/plugin-ask-user. Your code asks it with requestUserInput(), from a tool handler or anywhere else. Either way, <aparte-elicitation> presents it, and <aparte-chat> already contains one.

Open

A form asks one question at a time, with a chip per question; the composer’s button sends the answers once the last one is given.

Open

Once answered, the panel is gone. The plugin leaves a receipt in the reply instead: each question with its answer, so scrolling back shows what was asked.

Let the model ask:

ts
import { setupAskUser } from '@aparte/plugin-ask-user';

setupAskUser(); // registers the ask_user tool and its receipt

Or ask yourself:

ts
import { requestUserInput } from '@aparte/core';

const result = await requestUserInput({
  message: 'Which format should the report use?',
  schema: {
    type: 'enum',
    options: [
      { value: 'md', label: 'Markdown', recommended: true },
      { value: 'pdf', label: 'PDF' },
    ],
  },
});
if (result.action === 'accept') console.info(result.content); // 'md'

The schema is one field (enum, boolean, string) or an object of several, each with a header that names its chip. An enum can be multiple. The result is { action: 'accept', content } or { action: 'decline' } when the person skips; a request that ends without an answer (the turn was stopped, the signal fired) rejects with AparteElicitationAbortError.

html
<aparte-elicitation></aparte-elicitation>

Installation and the framework-specific traps: React · Vue · Svelte · Angular.

aparteGlobalConfig.setElicitationOptions() sets how questions are presented: allowOther adds a free-text “Other…” to every choice (on by default), layout asks a form stepped (one at a time) or stacked, and answerOnClick makes a single choice answer on the click (on by default) rather than with the composer’s button.

The ask_user tool is built, not fixed. setupAskUser() takes maxOptions (4 by default) and maxQuestions (5), the tool’s name, description and systemPrompt (or false to send none), and receipt: false for an app that records the exchange itself. These are AskUserSetupOptions, which extends AskUserToolOptions, what createAskUserTool() takes.

To wire it by hand: createAskUserTool() and askUserHandler for the tool, questionReceiptRenderer and buildReceipt for the card. To draw the receipt your own way, receiptRows() turns a call’s ReceiptSource (its input, result and structured result) into ReceiptRows, one { question, answer, declined? } per question:

ts
import { aparteGlobalConfig } from '@aparte/core';
import { receiptRows } from '@aparte/plugin-ask-user';

aparteGlobalConfig.registerToolRenderer('ask_user', {
  render: (segment) => {
    const list = document.createElement('ul');
    for (const row of receiptRows({ input: segment.toolCall.input, result: segment.result, structuredResult: segment.structuredResult })) {
      list.append(Object.assign(document.createElement('li'), { textContent: `${row.question}: ${row.answer}` }));
    }
    return list;
  },
});

The tool returns the answer twice: as a sentence for the model, and as a value, AskUserStructuredResult, which the receipt reads. A skipped question returns the sentence ASK_USER_DECLINED; compare with the constant rather than a copy of it.

<aparte-elicitation> · class AparteElicitation

Methods

MethodDescription
aparteConfigChanged(next: AparteConfig, previous: AparteConfig): voidThe boundary above us appeared, changed, or went away — move the registration with it. connectedCallback alone is not enough and cannot be: registering is a WRITE, and under all four wrappers it happens before attachConfig runs, so it lands on the global singleton. requestUserInput() then resolves the instance config, finds nothing, and rejects the request — the model hears the user refuse a question the user never saw. See AparteConfigAware.

CSS variables

VariableDefaultDescription
--aparte-elic-gapvar(--aparte-space-3)Vertical gap between the panel’s rows (message, body, tabs).
--aparte-elic-paddingvar(--aparte-space-3) var(--aparte-space-2)Padding inside the panel.
--aparte-elic-max-height50vhCap on the panel’s height; its body scrolls, the panel does not.
--aparte-elic-field-gapvar(--aparte-space-4)Space and separator padding between two fields of an object schema.
--aparte-elic-message-sizevar(--aparte-font-size-base)Font size of the question text at the top of the panel.
--aparte-elic-title-sizevar(--aparte-font-size-base)Font size of a field’s title.
--aparte-elic-desc-sizevar(--aparte-font-size-sm)Font size of a field’s description.
--aparte-elic-option-paddingvar(--aparte-space-4) var(--aparte-space-5)Padding of one enum/boolean option row.
--aparte-elic-option-radiusvar(--aparte-radius-md)Corner radius of an option row.
--aparte-elic-option-title-sizevar(--aparte-font-size-md)Font size of an option’s label, and of the text inputs.
--aparte-elic-option-desc-sizevar(--aparte-font-size-sm)Font size of an option’s secondary line.
--aparte-elic-control-size15pxSize of the radio/checkbox control in an option row.
--aparte-elic-input-radiusvar(--aparte-radius-md)Corner radius of the text inputs and of the Skip button.
--aparte-elic-textarea-min-height64pxMinimum height of a multi-line string field.
--aparte-elic-input-size0.85remFont size of the approval panel’s instruction field (the free-text note the user writes).
--aparte-elic-skip-sizevar(--aparte-font-size-md)Font size of the corner “Skip” affordance (the decline).
--aparte-elic-step-sizevar(--aparte-font-size-sm)Font size of a step tab, when the schema is asked one field at a time.
--aparte-elic-step-paddingvar(--aparte-space-2) var(--aparte-space-1)Padding of a step tab.
--aparte-elic-step-gapvar(--aparte-space-7)Gap between step tabs.
--aparte-elic-step-underline2pxThickness of the current step’s underline (a tab, not a pill).
--aparte-elic-dismiss-room72pxSpace the tab rail and the question message keep clear for the corner escape. Widen it for a locale whose “Skip” word is wider.
--aparte-approval-gap4pxGap between the stacked options of an approval request.
--aparte-approval-option-size0.85remFont size of an approval option button.
--aparte-approval-option-padding8px 10pxPadding of an approval option button.
--aparte-approval-option-radius8pxCorner radius of an approval option button.
--aparte-approval-args-max-height8.5remHeight cap of the arguments block before it scrolls.
--aparte-approval-args-sizevar(--aparte-font-size-sm)Font size of the arguments block.

<aparte-ask-user> is the same element under a name that says what it is for, registered by importing the plugin. Mount one or the other, never both.

  • A single choice is a radio group, several are a group of checkboxes, named by the question.
  • A form’s chips are a tab list: arrow keys move between questions.
  • “Other…” and a text answer are labelled fields; nothing in the panel is a bare input.
  • The panel takes the focus and gives it back when it closes (to the composer when there is nothing to go back to). From the chat’s own editor it focuses the panel itself, named by the question, so the next key does not pick an option. A reader writing in a field elsewhere on the page, or in another chat, keeps the focus, and a polite status says a question waits. Taken from nowhere, it does not scroll the page.
  • Approval — the same panel asking for a decision.
  • Composer — where the panel opens.