Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent. | 1 | /** |
| 2 | * An agent's face (docs.g1t.sh/guides/agents/, "Its face"): the little bot | |
| 3 | * every agent wears when it has no uploaded picture. A face is a `look`, | |
| 4 | * seven choices the agent's owner can make; an agent with no look of its | |
| 5 | * own wears the one drawn from its `avatar_seed`, so every agent has a | |
| 6 | * stable face from the moment it is made, and keeps it through a rename. | |
| 7 | * | |
| 8 | * The site draws the face (apps/web/app/components/agent-face.tsx) and the | |
| 9 | * agents service keeps it (the `look` column, JSON). No value imports, so | |
| 10 | * services test it under Node as it is. Wire shapes are snake_case. | |
| 11 | */ | |
| 12 | ||
| 13 | export const FACE_SHAPES = ["round", "square", "squircle", "blob", "hex"] as const; | |
| 14 | /** Ten named colours, each with a light and a dark rendering that read on both themes. */ | |
| 15 | export const FACE_COLORS = ["lavender", "mint", "peach", "sky", "pink", "lemon", "teal", "coral", "moss", "slate"] as const; | |
| 16 | export const FACE_EYES = ["dots", "round", "wide", "happy", "visor", "sleepy"] as const; | |
| 17 | export const FACE_MOUTHS = ["smile", "grin", "flat", "open", "none"] as const; | |
| 18 | export const FACE_ANTENNAS = ["none", "single", "double", "bulb"] as const; | |
| 19 | export const FACE_ACCESSORIES = ["none", "headphones", "bow", "glasses", "hat", "spark"] as const; | |
| 20 | export const FACE_PATTERNS = ["none", "stripe", "dots", "gradient"] as const; | |
| 21 | ||
| 22 | export type FaceShape = (typeof FACE_SHAPES)[number]; | |
| 23 | export type FaceColor = (typeof FACE_COLORS)[number]; | |
| 24 | export type FaceEyes = (typeof FACE_EYES)[number]; | |
| 25 | export type FaceMouth = (typeof FACE_MOUTHS)[number]; | |
| 26 | export type FaceAntenna = (typeof FACE_ANTENNAS)[number]; | |
| 27 | export type FaceAccessory = (typeof FACE_ACCESSORIES)[number]; | |
| 28 | export type FacePattern = (typeof FACE_PATTERNS)[number]; | |
| 29 | ||
| 30 | /** What an agent's face is made of. Every field is one of its listed choices. */ | |
| 31 | export type AgentLook = { | |
| 32 | shape: FaceShape; | |
| 33 | color: FaceColor; | |
| 34 | eyes: FaceEyes; | |
| 35 | mouth: FaceMouth; | |
| 36 | antenna: FaceAntenna; | |
| 37 | accessory: FaceAccessory; | |
| 38 | pattern: FacePattern; | |
| 39 | }; | |
| 40 | ||
| 41 | /** Each part of a look and its choices, in the order the Face editor shows them. */ | |
| 42 | export const LOOK_OPTIONS: { [K in keyof AgentLook]: readonly AgentLook[K][] } = { | |
| 43 | shape: FACE_SHAPES, | |
| 44 | color: FACE_COLORS, | |
| 45 | eyes: FACE_EYES, | |
| 46 | mouth: FACE_MOUTHS, | |
| 47 | antenna: FACE_ANTENNAS, | |
| 48 | accessory: FACE_ACCESSORIES, | |
| 49 | pattern: FACE_PATTERNS, | |
| 50 | }; | |
| 51 | ||
| 52 | export const LOOK_KEYS = Object.keys(LOOK_OPTIONS) as (keyof AgentLook)[]; | |
| 53 | ||
| 54 | /** FNV-1a, 32 bits: a seed as a number. */ | |
| 55 | function fnv(text: string): number { | |
| 56 | let h = 0x811c9dc5; | |
| 57 | for (let i = 0; i < text.length; i++) { | |
| 58 | h ^= text.charCodeAt(i); | |
| 59 | h = Math.imul(h, 0x01000193); | |
| 60 | } | |
| 61 | return h >>> 0; | |
| 62 | } | |
| 63 | ||
| 64 | /** One choice for a part, from the seed and the part's name: parts vary independently. */ | |
| 65 | function pick<T>(seed: string, part: string, options: readonly T[], weights?: readonly number[]): T { | |
| 66 | const roll = fnv(`${part}:${seed}`) >>> 8; | |
| 67 | if (!weights) return options[roll % options.length]!; | |
| 68 | const total = weights.reduce((sum, w) => sum + w, 0); | |
| 69 | let at = roll % total; | |
| 70 | for (let i = 0; i < options.length; i++) { | |
| 71 | at -= weights[i]!; | |
| 72 | if (at < 0) return options[i]!; | |
| 73 | } | |
| 74 | return options[options.length - 1]!; | |
| 75 | } | |
| 76 | ||
| 77 | /** | |
| 78 | * The face an agent wears when it has chosen none: the same for the same | |
| 79 | * seed everywhere, and different from its neighbours' as far as seven | |
| 80 | * parts allow. Plainer choices (no antenna, no accessory, no pattern) are | |
| 81 | * weighted so faces read as a set rather than a costume party, and `none` | |
| 82 | * never lands on both the eyes' expression and the mouth. | |
| 83 | */ | |
| 84 | export function lookFromSeed(seed: string): AgentLook { | |
| 85 | const s = seed || "agent"; | |
| 86 | const look: AgentLook = { | |
| 87 | shape: pick(s, "shape", FACE_SHAPES, [3, 2, 4, 2, 2]), | |
| 88 | color: pick(s, "color", FACE_COLORS), | |
| 89 | eyes: pick(s, "eyes", FACE_EYES, [3, 3, 2, 2, 2, 1]), | |
| 90 | mouth: pick(s, "mouth", FACE_MOUTHS, [4, 2, 2, 2, 1]), | |
| 91 | antenna: pick(s, "antenna", FACE_ANTENNAS, [4, 3, 2, 2]), | |
| 92 | accessory: pick(s, "accessory", FACE_ACCESSORIES, [7, 1, 1, 1, 1, 1]), | |
| 93 | pattern: pick(s, "pattern", FACE_PATTERNS, [6, 1, 1, 2]), | |
| 94 | }; | |
| 95 | if (look.mouth === "none" && look.eyes === "visor") look.mouth = "flat"; | |
| 96 | return look; | |
| 97 | } | |
| 98 | ||
| 99 | /** | |
| 100 | * A look as it was sent or stored, checked: every part present and one of | |
| 101 | * its choices. Null when it is not one (a service refuses it, a page draws | |
| 102 | * the seed's face instead). | |
| 103 | */ | |
| 104 | export function readLook(raw: unknown): AgentLook | null { | |
| 105 | const value = typeof raw === "string" ? parse(raw) : raw; | |
| 106 | if (!value || typeof value !== "object" || Array.isArray(value)) return null; | |
| 107 | const given = value as Record<string, unknown>; | |
| 108 | const look: Partial<AgentLook> = {}; | |
| 109 | for (const key of LOOK_KEYS) { | |
| 110 | const options = LOOK_OPTIONS[key] as readonly string[]; | |
| 111 | const choice = given[key]; | |
| 112 | if (typeof choice !== "string" || !options.includes(choice)) return null; | |
| 113 | (look as Record<string, string>)[key] = choice; | |
| 114 | } | |
| 115 | return look as AgentLook; | |
| 116 | } | |
| 117 | ||
| 118 | function parse(text: string): unknown { | |
| 119 | try { | |
| 120 | return JSON.parse(text); | |
| 121 | } catch { | |
| 122 | return null; | |
| 123 | } | |
| 124 | } | |
| 125 | ||
| 126 | /** Whether two looks are the same face. */ | |
| 127 | export function sameLook(a: AgentLook | null | undefined, b: AgentLook | null | undefined): boolean { | |
| 128 | if (!a || !b) return a == b; | |
| 129 | return LOOK_KEYS.every((key) => a[key] === b[key]); | |
| 130 | } | |
| 131 | ||
| 132 | /** The face an agent shows: its own look, else the one its seed draws. */ | |
| 133 | export function lookOf(agent: { look?: AgentLook | null; avatar_seed?: string | null; id?: string | null; handle?: string | null; name?: string | null }): AgentLook { | |
| 134 | return agent.look ?? lookFromSeed(agent.avatar_seed || agent.id || agent.handle || agent.name || "agent"); | |
| 135 | } | |
| 136 | ||
| 137 | /** | |
| 138 | * A random look, for the Shuffle button: `random` is `Math.random` unless | |
| 139 | * a test hands in its own. Every part is drawn evenly, so shuffling shows | |
| 140 | * the whole range. | |
| 141 | */ | |
| 142 | export function randomLook(random: () => number = Math.random): AgentLook { | |
| 143 | const draw = <T>(options: readonly T[]): T => options[Math.min(options.length - 1, Math.floor(random() * options.length))]!; | |
| 144 | return { | |
| 145 | shape: draw(FACE_SHAPES), | |
| 146 | color: draw(FACE_COLORS), | |
| 147 | eyes: draw(FACE_EYES), | |
| 148 | mouth: draw(FACE_MOUTHS), | |
| 149 | antenna: draw(FACE_ANTENNAS), | |
| 150 | accessory: draw(FACE_ACCESSORIES), | |
| 151 | pattern: draw(FACE_PATTERNS), | |
| 152 | }; | |
| 153 | } | |
| 154 | ||
| 155 | /** | |
| 156 | * What a personality preset suggests for a face that has not been chosen: | |
| 157 | * a precise voice looks precise. Only a suggestion, applied by the Face | |
| 158 | * editor when the person has made no choice; it never changes a look an | |
| 159 | * agent already has. | |
| 160 | */ | |
| 161 | export const PRESET_EXPRESSIONS: Record<string, Partial<AgentLook>> = { | |
| 162 | crisp: { eyes: "dots", mouth: "flat" }, | |
| 163 | friendly: { eyes: "happy", mouth: "smile" }, | |
| 164 | socratic: { eyes: "round", mouth: "open" }, | |
| 165 | terse: { eyes: "visor", mouth: "flat" }, | |
| 166 | }; | |
| 167 | ||
| 168 | /** The seed's face with the preset's expression laid over it, for an agent that has chosen no look. */ | |
| 169 | export function suggestedLook(seed: string, preset: string | null | undefined): AgentLook { | |
| 170 | return { ...lookFromSeed(seed), ...(preset ? (PRESET_EXPRESSIONS[preset] ?? {}) : {}) }; | |
| 171 | } |