Skip to content
kerfjsGitHub

Assistant

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

SourcePackage @kerf-js/veneer

Preview

tsx
import { Assistant, foldAnswer } from '@kerf-js/veneer'import type { AgentEvent } from '@kerf-js/veneer'import type { AgentSource } from '@kerf-js/veneer'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: 'kerf 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 @kerf-js/veneer

Usage

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

Failed

Retry runs it again.

tsx
import { state } from '@kerf-js/core'import { Assistant, foldAnswer } from '@kerf-js/veneer'import type { State } from '@kerf-js/core'import type { AgentEvent, Answer } from '@kerf-js/veneer'export function Failed() {  const answer = state(    foldAnswer([      {        kind: 'text',        text: 'Reading the log for `d-4819`',      },      {        kind: 'error',        message: 'The connection dropped.',      },    ]),  )  const regenerate = () => {    answer.value = foldAnswer([])    play(answer, [      {        kind: 'text',        text:          'Deploy `d-4819` failed its health ' +          'check in ap-south.',      },      { kind: 'done' },    ])  }  return (    <Assistant      answer={answer.value}      onRegenerate={regenerate}    />  )}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 '@kerf-js/veneer'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 '@kerf-js/veneer'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 '@kerf-js/core'import {  Assistant,  Button,  Stack,  foldAnswer} from '@kerf-js/veneer'import type { State } from '@kerf-js/core'import type { AgentEvent, Answer } from '@kerf-js/veneer'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