• English
  • States

    States come in four groups, and every one is live below, sharing a single stage. On the default cute face each state has its own eyes, mouth and blush, and some throw off an effect (see Expressions); in line face mode each state also wears a mouth from STATE_MOUTH.

    Resting

    idleIdle
    sleepingSleeping
    drowsyDrowsy
    boredBored
    wakingWaking
    powering-downPowering down

    Attending

    listeningListening
    thinkingThinking
    searchingSearching
    readingReading
    writingWriting
    workingWorking
    loadingLoading
    progressProgress
    radarScanning
    orbitOrbiting
    dictatingDictating
    uploadingUploading
    sendingSending
    receivingReceiving
    messagingMessaging
    waitingWaiting
    spawningSpawning
    composingComposing
    draggingDragging

    Feeling

    happyHappy
    excitedExcited
    laughingLaughing
    playfulPlayful
    proudProud
    celebrateCelebrating
    bouncingBouncing
    shyShy
    sadSad
    angryAngry
    confusedConfused
    suspiciousSuspicious
    surprisedSurprised
    scaredScared
    curiousCurious
    hummingHumming

    Signalling

    alertingAlerting
    notifyingNotifying
    refusingRefusing

    What a state defines

    TableRole
    STATE_CUTE_EYESEyes the cute face cycles through
    STATE_CUTE_MOUTHMouth of the cute face
    STATE_BLUSHBlush strength of the cute face
    STATE_FXMood or activity effect outside the body
    STATE_EYE_TOPOLOGIESExpressions of the eyes and line faces
    STATE_EYE_SWITCH_MSRandomised interval between them
    STATE_BLINK_MSBlink interval, or null for states that do not blink
    STATE_FACE_TUNEFace adjustments layered over the default
    STATE_GAZEGaze drift amplitude and how often it retargets
    STATE_BELTBelt overlay of the eyes and line faces
    STATE_MOUTHMouth worn in line-face mode
    STATE_ENTRY_EFFECTSOne-off spin, bounce or burst on entry
    behaviourA function returning body targets each frame

    Body motion

    A behaviour returns four numbers per frame — tilt, x, y and squash — which springs then chase. Because springs do the smoothing, a behaviour can jump its targets without producing a visible jump.

    idle is the one worth reading:

    ({ clock }) => ({
      tilt: Math.sin(clock * 0.5) * 1.5 + Math.sin(clock * 0.17) * 0.6,
      x: Math.sin(clock * 0.27),
      y: Math.sin(clock * 0.85) * 1.2,
      squash: 1 + Math.sin(clock * 0.85) * 0.007,
    });

    Three sine waves at unrelated frequencies. Because the periods never line up, the motion has no perceptible loop — which is what stops a character sitting in a sidebar for ten minutes from looking like a repeating GIF.

    Impulses

    Some states need discrete events rather than a continuous curve. angry kicks the vertical spring every 1.8 to 3.2 seconds; listening nods. These add velocity directly instead of moving a target:

    impulse(70); // adds to the y spring's velocity

    The spring absorbs the kick and settles, which produces a physical recoil that would be tedious to author as a curve.

    Spring channels

    Every continuous quantity is a damped spring integrated at a fixed 120 Hz substep, regardless of display refresh rate.

    ChannelωζDrives
    tilt50.9Body rotation, scaled by tiltScale
    x3.51Horizontal drift
    y41Vertical drift and impulses
    squash100.8Vertical scale
    blink261Eye opening
    eyeScale90.85Emphasis
    gazeX / gazeY131Gaze offset
    eyeMorph81Expression transition
    spin6.21Imperative turn about the vertical axis
    cuteBlend101Cross-over to and from the cute face
    blush81Blush strength
    fx71Effect fade-in
    beltBlend141Body-to-belt transition
    beltFade111Cross-fade between two belt kinds
    hudBlend121HUD dial fade in and out
    badge90.55Notification dot

    The fixed substep is why a 144 Hz monitor and a 60 Hz monitor converge identically. Integrating with the frame delta directly would make the character settle faster on a faster display.

    Belt states on the cute face

    On the eyes and line faces, sixteen states hand their body over to a belt overlay (STATE_BELT): the body morphs to a circle, shrinks to a core, and what the state means is carried by the elements moving around it. On the default cute face that would lose the face, so they do not shrink the body: the whole character, face included, stays, and the same meaning is said by an effect beside it (STATE_FX) — rising dots for thinking, a small filling bar at the top right for progress, a pencil for writing. powering-down dims the body and bouncing hops it.

    sleepingSleepingzz
    drowsyDrowsyzz
    thinkingThinkingdots
    writingWritingpencil
    workingWorkingarc
    loadingLoadingspinner
    progressProgressprogress
    radarScanningripple
    orbitOrbitingorbit
    dictatingDictatingwaves
    uploadingUploadingrise
    sendingSendingsend
    receivingReceivingfall
    messagingMessagingbubbles
    waitingWaitingellipsis
    spawningSpawninggather
    happyHappyheart
    proudProudsparkle
    celebrateCelebratingsparkle
    sadSadtear
    surprisedSurprisedstrokes
    scaredScaredstrokes
    alertingAlertingbang
    refusingRefusingbang

    Like the mood effects these are pure functions of time, carried on the snapshot as fx, so the vector, pixel and kaomoji series agree automatically. On the cute face snapshot.belt is always null.

    Belts

    The eyes and line faces draw these states with a belt: the body collapses to a core and the belt takes over. See belts and particles for how the transition works and how each belt is drawn. The demo below uses face="eyes" so the belts themselves are visible.

    dotsthinking
    orbitorbit
    orbitmessaging
    orbitwaiting
    radarradar
    progressprogress
    gatherspawning
    wavedictating
    sendsending
    receivereceiving
    dockuploading
    ballbouncing
    whirlloading
    standbypowering-down
    pencilwriting
    bangalerting
    import { MarkStageProvider, StagedMark, STATE_BELT } from '@aora-bot/react';
    
    export default function Demo() {
      const entries = Object.entries(STATE_BELT) as [string, string][];
      return (
        <MarkStageProvider maxSources={entries.length}>
          <div className="site-demo">
            {entries.map(([state, belt]) => (
              <div className="site-demo__cell" key={state}>
                <StagedMark shape="mochi" state={state as never} color="blue" face="eyes" sizePx={62} />
                <span className="site-demo__label">{belt}</span>
                <span className="site-demo__label" style={{ opacity: 0.6 }}>
                  {state}
                </span>
              </div>
            ))}
          </div>
        </MarkStageProvider>
      );
    }

    Belts are pure functions of time. Nothing accumulates, so a belt can be paused, scrubbed or resumed at any point and remain correct — which is what lets hundreds of characters be frozen without drifting out of sync when they resume.

    At a glance

    Every state in one table: the eyes it cycles, its mouth, its effect (cute face) or the belt it hands its body to (eyes and line faces), the protocol ids that reach it, and a dial that tends to sit well beside it. All but the last column are read from the engine's tables at render time; the HUD column is a suggestion, since a dial is data the host has and a state is what the agent is doing, and the engine deliberately keeps the two independent.

    Resting

    StateEyesMouthEffectIdHUD
    idleIdledot → dot → dot → dotLeft → dot → dotRightnone—02—
    sleepingSleepingcurvenonezz00clock
    drowsyDrowsycurveSleepy → dotHalf → curveSleepynonezz0615—
    boredBoreddotHalf → dotLeft → dotHalf → dotRightflat—04—
    wakingWakingcurve → dotHalf → dotdot—0107—
    powering-downPowering downdot → dotHalf → curvenone—41—

    Attending

    StateEyesMouthEffectIdHUD
    listeningListeningdotLeft → dot → dotLeftnone—3537—
    thinkingThinkingdotLook → dotUp → dotLookflatdots30context
    searchingSearchingdotLeft → dotRight → dotUp → dotdot—40—
    readingReadingdotLeft → dotRightnone—42—
    writingWritingdotDown → dotnonepencil43—
    workingWorkingdotDown → dot → dotDownnonearc1632gauge
    loadingLoadingdot → dotLooknonespinner0536—
    progressProgressdotnoneprogress47progress
    radarScanningdot → dotLeft → dotRightnoneripple51—
    orbitOrbitingdot → dotLooknoneorbit49—
    dictatingDictatingdotLeft → dotowaves46voice
    uploadingUploadingdotUp → dotnonerise45sync
    sendingSendingdot → archsmilesend44—
    receivingReceivingdotWide → dotdotfall31—
    messagingMessagingdot → archcatbubbles52notify
    waitingWaitingdot → dotLeft → dot → dotRightnoneellipsis48timer
    spawningSpawningdotWide → dotdotgather08—
    composingComposingdotDown → dotcat—39—
    draggingDraggingdotRight → dot → dotRighto—09—

    Feeling

    StateEyesMouthEffectIdHUD
    happyHappyarch → archWide → archcatheart10—
    excitedExciteddotWide → archWidegrin—22—
    laughingLaughingarchWide → archlaugh—23—
    playfulPlayfularch → wink → archWide → dotsmirk—24—
    proudProuddotUp → archsmilesparkle19—
    celebrateCelebratingstar → archWide → wink → archgrinsparkle2533kpi
    bouncingBouncingarch → dotWidesmile—26—
    shyShydotDown → dotHalfcat—14—
    sadSadsadfrowntear12—
    angryAngrymadfrown—21—
    confusedConfuseddotSuspicious → dotLook → dotHalfwavy—1120—
    suspiciousSuspiciousdotSuspicious → dotHalfflat—18—
    surprisedSurpriseddotWideostrokes13—
    scaredScareddotWide → dotSmallwavystrokes17—
    curiousCuriousdotWide → dotLeft → dotWide → dotRightdot—03—
    hummingHummingcurve → dotHalf → curve → dotsmile—27—

    Signalling

    StateEyesMouthEffectIdHUD
    alertingAlertingdotWide → dotobang34—
    notifyingNotifyingdotWide → arch → dotdot—50notify
    refusingRefusingcross → dotHalfwavybang38—

    Mapping application state

    Most applications do not think in these terms. Two helpers translate.

    toolstate
    searchsearching
    web_searchsearching
    grepsearching
    globsearching
    browsereading
    fetchreading
    read_filereading
    readreading
    write_filewriting
    writewriting
    editwriting
    str_replacewriting
    runworking
    shellworking
    bashworking
    terminalworking
    uploaduploading
    thinkthinking
    planthinking
    asklistening
    waitwaiting
    { awaitingUser: true }waiting
    { running: true }working
    import { characterStateForAgent, activityFromTool, TOOL_ACTIVITIES } from '@aora-bot/react';
    
    export default function Demo() {
      return (
        <table>
          <thead>
            <tr>
              <th>tool</th>
              <th>state</th>
            </tr>
          </thead>
          <tbody>
            {Object.keys(TOOL_ACTIVITIES).map((tool) => (
              <tr key={tool}>
                <td>
                  <code>{tool}</code>
                </td>
                <td>
                  <code>{activityFromTool(tool).state}</code>
                </td>
              </tr>
            ))}
            <tr>
              <td>
                <code>{'{ awaitingUser: true }'}</code>
              </td>
              <td>
                <code>{characterStateForAgent({ awaitingUser: true })}</code>
              </td>
            </tr>
            <tr>
              <td>
                <code>{'{ running: true }'}</code>
              </td>
              <td>
                <code>{characterStateForAgent({ running: true })}</code>
              </td>
            </tr>
          </tbody>
        </table>
      );
    }

    Next