Skip to content
weftGitHub

Assistant

One answer from the assistant, drawn from its stream of events.

SourcePackage @weft-js/warp

Preview

tsx
import { state } from '@weft-js/core'import { Assistant, foldAnswer } from '@weft-js/warp'import type { AgentEvent } from '@weft-js/warp'import type { AgentSource } from '@weft-js/warp'export function Answer() {  return (    <Assistant answer={FINISHED} n={1} quiet onFeedback={() => {}} onRegenerate={() => {}} />  )}const DEPLOY_SOURCES: AgentSource[] = [  { title: 'd-4819 build log', path: 'mobile-bff · 15:27', snippet: 'health check failed: upstream timed out after 3000 ms in ap-south' },  { title: 'Health checks', path: 'weft docs · Deploys', snippet: 'A deploy that fails three checks in a row is held back from traffic.' },  { title: 'c44e190', path: 'mobile-bff · retry.ts · L18', snippet: 'const attempts = 5 // was 2' },]const WHY_TEXT = 'Deploy `d-4819` failed its health check. The new retry code tries five times on every timed out upstream call, so one slow request in ap-south holds a connection for 15 seconds [1]. Three failed checks in a row held the deploy back, so nobody was served the bad version [2].\n\nThe cause is one line in the retry settings [3]:\n\n```TypeScript\nconst attempts = 5 // was 2\n```\n\n- `d-4801` is still live and healthy.\n- Lowering `attempts` to 2 should get the deploy through.\n'const WHY_FAILED: AgentEvent[] = [  { kind: 'plan', steps: [{ id: 'logs', title: 'Read the build log', state: 'done' }, { id: 'diff', title: 'Compare with the last good deploy', state: 'done' }, { id: 'sum', title: 'Explain what went wrong', state: 'running' }] },  { kind: 'reasoning', at: 0, text: 'The deploy reached the health check, so the build itself is fine. The log should say which check failed and in which region.' },  { kind: 'tool', id: 'logs', name: 'read_logs', args: '{ "deploy": "d-4819", "lines": 200 }', at: 4000 },  { kind: 'tool-result', id: 'logs', output: '3 failed checks\nupstream timed out after 3000 ms (ap-south)', ms: 640 },  { kind: 'tool', id: 'diff', name: 'compare_deploys', args: '{ "from": "d-4801", "to": "d-4819" }' },  { kind: 'tool-result', id: 'diff', output: '1 file changed\nsrc/retry.ts: attempts 2 → 5', ms: 410 },  { kind: 'text', text: WHY_TEXT },  ...DEPLOY_SOURCES.map((s): AgentEvent => ({ kind: 'source', ...s })),  { kind: 'done' },]const FINISHED = foldAnswer(WHY_FAILED)

Installation

npm install @weft-js/warp

Usage

tsx
import { Assistant } from '@weft-js/warp'<Assistant answer={FINISHED} n={1} quiet onFeedback={() => {}} onRegenerate={() => {}} />

Failed

Retry runs it again.

tsx
import { state } from '@weft-js/core'import type { State } from '@weft-js/core'import { Assistant, foldAnswer } from '@weft-js/warp'import type { AgentEvent, Answer } from '@weft-js/warp'export function Failed() {  const answer = state(foldAnswer([{ kind: 'text', text: 'Reading the log for `d-4819`' }, { kind: 'error', message: 'The connection dropped.' }]))  return <Assistant answer={answer.value} onRegenerate={() => { answer.value = foldAnswer([]); play(answer, [{ kind: 'text', text: 'Deploy `d-4819` failed its health check in ap-south.' }, { kind: 'done' }]) }} />}function play(answer: State<Answer>, events: AgentEvent[]) {  let at = 0  const next = () => {    const event = events[at++]    if (!event || answer.value.state === 'stopped') return    answer.value = foldAnswer(answer.value, event)    setTimeout(next, 450)  }  setTimeout(next, 450)}

Stopped

What arrived is kept.

tsx
import { Assistant, foldAnswer } from '@weft-js/warp'export function Stopped() {  return (    <Assistant answer={CUT_OFF} quiet />  )}const CUT_OFF = foldAnswer([{ kind: 'text', text: 'Deploy `d-4819` failed its' }, { kind: 'done', stopped: true }])

Waiting for the first word

An assistant answer with no content yet shows that a reply is on its way.

tsx
import { Assistant, foldAnswer } from '@weft-js/warp'export function Waiting() {  return (    <Assistant answer={WAITING} />  )}const WAITING = foldAnswer([])

Waiting for approval

Approve or Deny carries on the run. You can edit the arguments first.

tsx
import { state } from '@weft-js/core'import type { State } from '@weft-js/core'import { Assistant, Button, Stack, foldAnswer } from '@weft-js/warp'import type { AgentEvent, Answer } from '@weft-js/warp'export function Approval() {  const answer = state(foldAnswer(ROLLBACK_ASK))  const reset = () => (answer.value = foldAnswer(ROLLBACK_ASK))  return (    <Stack gap="related">      <Assistant answer={answer.value} quiet onApprove={() => play(answer, ROLLBACK_RUN)} onDeny={() => play(answer, ROLLBACK_DENIED)} />      {answer.value.state === 'done' && <Button emphasis="ghost" tone="neutral" size="sm" onClick={reset}>Start again</Button>}    </Stack>  )}const ROLLBACK_ARGS = '{\n  "project": "mobile-bff",\n  "to": "d-4801",\n  "regions": ["ap-south"]\n}'const ROLLBACK_ASK: AgentEvent[] = [  { kind: 'reasoning', text: 'A rollback changes production, so I should check the target first and ask before running it.' },  { kind: 'tool', id: 'check', name: 'get_deploy', args: '{ "deploy": "d-4801" }' },  { kind: 'tool-result', id: 'check', output: 'd-4801 · live · healthy · 412 ms p95', ms: 280 },  { kind: 'text', text: '`d-4801` is healthy, so it is a safe place to go back to. This changes production, so I need your approval.' },  { kind: 'approval', id: 'rollback', name: 'rollback', args: ROLLBACK_ARGS },]const ROLLBACK_RUN: AgentEvent[] = [  { kind: 'tool', id: 'rollback' },  { kind: 'tool-result', id: 'rollback', output: 'mobile-bff is on d-4801 in ap-south', ms: 2100 },  { kind: 'text', text: 'Done. `mobile-bff` is back on `d-4801`, and the checks are passing.' },  { kind: 'done' },]const ROLLBACK_DENIED: AgentEvent[] = [  { kind: 'tool-result', id: 'rollback', denied: true },  { kind: 'text', text: 'Okay, I left production as it is.' },  { kind: 'done' },]function play(answer: State<Answer>, events: AgentEvent[]) {  let at = 0  const next = () => {    const event = events[at++]    if (!event || answer.value.state === 'stopped') return    answer.value = foldAnswer(answer.value, event)    setTimeout(next, 450)  }  setTimeout(next, 450)}

When to use

Use it for

  • Anything the assistant says: text, reasoning, tool calls, questions and sources in one turn

Accessibility

  • The end of an answer is announced once. Set quiet on turns that were already there on load, so they take no focus.

API Reference

PropTypeDefaultDescription
answerAnswer—The answer so far: foldAnswer(events), or the result of folding a stream.
nnumber—The turn number, used in its accessible name.
arrivebooleanfalsePlays the entrance, for a turn that just started.
quietbooleanfalseThe turn was there on load: approvals take no focus and nothing is announced.
render((text: string, answer: Answer) => unknown)—Render a block of answer text yourself (Markdown, say); the default is Prose.
alwaysAllow((tool: AgentTool) => string | undefined)—The label for the "always allow" box on an approval.
branch{ index: number; count: number; onChange: (index: number) => void; }—Which of several answers is shown, with arrows between them.
EventTypeDefaultDescription
onApprove((tool: AgentTool, args: string, always: boolean) => void)—Called when a tool call is approved, with its arguments as edited and whether to always allow it.
onDeny((tool: AgentTool) => void)—Called when a tool call is denied.
onAnswer((id: string, answer: string | string[]) => void)—Called with a question's id and the answer picked or typed.
onFeedback((value: Feedback, reason?: string | undefined) => void)—Called with up, down or null when thumbs change, and the reason picked for a thumbs down.
onRegenerate(() => void)—Adds Regenerate: ask for a new answer.
onBranch(() => void)—Adds Branch: start a new conversation from this answer.

Parts

Every element carries a stable name, so you can style it from your own CSS: [data-scope="assistant"] [data-part="mark"]

markcontent

RelatedTool callProseSourcesMessage actions