import type { ThreadSnapshot } from "@rakazo/contracts"; import { type EvooPose, mapPersonaArchetypeToIdlePose, mapVoicePhaseToPose } from "@rakazo/ui-web"; import type { StagePresencePhase } from "../components/stage/StageHero"; import type { VoicePhase } from "../components/voice/VoiceStage"; import type { GroupSpark } from "./group-stage"; /** * Template-aligned archetypes — derived from bot fields, not a DB column. * * Personalization bar (Ingo): lifestyle-agnostic crew fit — office, scene, nerd, all the same * mechanism. Tone is geek-peer OR quiet-doer, never condescending Wunderwuzzi. Onboarding harvest * suggests lanes (`user-help-vibe.ts`); spawn/select specialists accordingly. See * `docs/PERSONA-SELECTION.md`. * * Relationship over time: always-on presence, compounding via use, never creepy, not yes-men — * `docs/OO-RELATIONSHIP.md`, seeded in user MEMORY.md. */ export type PersonaArchetype = | "cto-executive" | "chief-operator" | "builder" | "curator" | "inbox-work" | "inbox-life" | "home-steward" | "quant" | "engineer" | "specialist"; export type PersonaCadence = "deliberate" | "steady" | "urgent"; export interface BotPersonaInput { name: string; title: string; description: string; } export interface BotPersonaProfile { archetype: PersonaArchetype; cadence: PersonaCadence; /** Short role line shown under the bot name in voice mode. */ roleLine: string; bloomAccent: "cyan" | "emerald" | "violet" | "amber" | "rose"; /** Preferred idle pose when not driven by runtime status. */ idlePose: EvooPose; } export interface VoiceAwarenessEvent { kind: "subagent" | "handoff" | "delegation" | "peer-reply" | "working"; actorName: string; detail: string; targetName?: string; } const ARCHETYPE_ORDER: readonly PersonaArchetype[] = [ "cto-executive", "chief-operator", "builder", "curator", "inbox-work", "inbox-life", "home-steward", "quant", "engineer", "specialist", ] as const; const ARCHETYPE_RULES: ReadonlyArray<{ archetype: PersonaArchetype; match: (haystack: string) => boolean; }> = [ { archetype: "cto-executive", match: (h) => h.includes("steve jobs") || h.includes("autonomous cto") || h.includes("cto-steve"), }, { archetype: "chief-operator", match: (h) => h.includes("fixer") || h.includes("chief of staff") || h.includes("chief-of-staff") || (h.includes("chief") && !h.includes("technology")), }, { archetype: "builder", match: (h) => h.includes("eggbot") || h.includes("dr. egg") || h.includes("bot builder"), }, { archetype: "curator", match: (h) => h.includes("webby") || h.includes("website admin") || h.includes("newsletter"), }, { archetype: "inbox-work", match: (h) => (h.includes("work") && h.includes("inbox")) || h.includes("templates/work") || h === "work", }, { archetype: "inbox-life", match: (h) => (h.includes("life") && h.includes("inbox")) || h.includes("templates/life") || h === "life", }, { archetype: "home-steward", match: (h) => h.includes("home-robot") || h.includes("home robot") || h.includes("matter") || h.includes("smart home"), }, { archetype: "quant", match: (h) => h.includes("apex") || h.includes("quant") || h.includes("trad") || h.includes("portfolio") || h.includes("alpha"), }, { archetype: "engineer", match: (h) => h.includes("architect") || h.includes("devops") || h.includes("security") || h.includes("infra") || h.includes("code ") || h.endsWith(" code") || h.includes("engineer"), }, ]; const PROFILE: Record> = { "cto-executive": { cadence: "deliberate", bloomAccent: "violet", idlePose: "analyzing", }, "chief-operator": { cadence: "steady", bloomAccent: "amber", idlePose: "collaborating", }, builder: { cadence: "steady", bloomAccent: "cyan", idlePose: "curious", }, curator: { cadence: "deliberate", bloomAccent: "emerald", idlePose: "reading", }, "inbox-work": { cadence: "urgent", bloomAccent: "cyan", idlePose: "waiting", }, "inbox-life": { cadence: "steady", bloomAccent: "rose", idlePose: "concerned", }, "home-steward": { cadence: "steady", bloomAccent: "emerald", idlePose: "listening", }, quant: { cadence: "urgent", bloomAccent: "cyan", idlePose: "analyzing", }, engineer: { cadence: "steady", bloomAccent: "cyan", idlePose: "working", }, specialist: { cadence: "steady", bloomAccent: "emerald", idlePose: "reading", }, }; export function resolvePersonaArchetype(input: BotPersonaInput): PersonaArchetype { const haystack = [input.name, input.title, input.description].join(" ").toLowerCase(); for (const rule of ARCHETYPE_RULES) { if (rule.match(haystack)) return rule.archetype; } return "specialist"; } export function resolveBotPersona(input: BotPersonaInput): BotPersonaProfile { const archetype = resolvePersonaArchetype(input); const roleLine = input.title.trim() || fallbackRoleLine(archetype); return { archetype, roleLine, ...PROFILE[archetype] }; } function fallbackRoleLine(archetype: PersonaArchetype): string { switch (archetype) { case "cto-executive": return "Autonomous CTO"; case "chief-operator": return "Chief of Staff"; case "builder": return "Bot Architect"; case "curator": return "Site Curator"; case "inbox-work": return "Work Inbox"; case "inbox-life": return "Life Inbox"; case "home-steward": return "Home Steward"; case "quant": return "Quant Lead"; case "engineer": return "Engineer"; default: return "Specialist"; } } export function resolveTextStagePhase(input: { running: boolean; botStatus?: string; streaming?: boolean; hasMessages: boolean; awareness?: VoiceAwarenessEvent | null; }): StagePresencePhase { if (input.botStatus === "error" || input.botStatus === "failed") return "error"; if (input.awareness?.kind === "subagent" || input.awareness?.kind === "handoff") { return "thinking"; } if (input.running) return "working"; if (input.streaming) return "speaking"; if (input.botStatus === "waiting_input") return "listening"; if (!input.hasMessages) return "idle"; return "idle"; } export function personaTextPose( profile: BotPersonaProfile, phase: StagePresencePhase, collaborating = false, ): EvooPose { if (collaborating) return "collaborating"; switch (phase) { case "working": return "working"; case "listening": return "listening"; case "speaking": return "speaking"; case "thinking": return "thinking"; case "error": return "error"; default: return profile.idlePose ?? mapPersonaArchetypeToIdlePose(profile.archetype); } } export function personaBloomGlow(accent: BotPersonaProfile["bloomAccent"], energy = 0): string { const alpha = 0.12 + energy * 0.28; switch (accent) { case "violet": return `rgba(167,139,250,${alpha})`; case "amber": return `rgba(251,191,36,${alpha})`; case "rose": return `rgba(251,113,133,${alpha})`; case "cyan": return `rgba(34,211,238,${alpha})`; default: return `rgba(52,211,153,${alpha})`; } } export function personaMascotPose( profile: BotPersonaProfile, phase: VoicePhase, collaborating = false, ): EvooPose { if (collaborating) return "collaborating"; const voicePose = mapVoicePhaseToPose(phase, collaborating); if (phase === "listening") return voicePose; if (phase === "speaking") return voicePose; if (phase === "thinking") return voicePose; return profile.idlePose ?? mapPersonaArchetypeToIdlePose(profile.archetype); } export function cadencePulseMs(cadence: PersonaCadence): number { switch (cadence) { case "deliberate": return 4200; case "urgent": return 2400; default: return 3200; } } /** Recent collaboration / delegation signals for voice awareness strip. */ export function extractVoiceAwareness( snapshot: ThreadSnapshot | null, botName: string, ): VoiceAwarenessEvent | null { if (!snapshot) return null; const messages = snapshot.messages.slice(-12).reverse(); for (const message of messages) { for (let index = message.blocks.length - 1; index >= 0; index -= 1) { const block = message.blocks[index]; if (!block) continue; if (block.kind === "subagent" && block.status === "running") { return { kind: "subagent", actorName: botName, detail: block.task.trim(), targetName: block.name.trim() || undefined, }; } if (block.kind === "handoff") { return { kind: "handoff", actorName: botName, detail: block.text.trim(), }; } if (block.kind === "bot_message_sent") { return { kind: "delegation", actorName: botName, detail: block.text.trim(), targetName: block.toBotName.trim(), }; } if (block.kind === "bot_message_received") { return { kind: "peer-reply", actorName: block.fromBotName.trim(), detail: block.text.trim(), }; } } } if (snapshot.run && ["running", "queued", "leased"].includes(snapshot.run.status)) { return { kind: "working", actorName: botName, detail: "", }; } return null; } export function narrateAwarenessForPersona( profile: BotPersonaProfile, event: VoiceAwarenessEvent, ): string { const specialist = event.targetName ?? "a specialist"; const task = event.detail ? truncate(event.detail, 96) : ""; switch (event.kind) { case "subagent": return narrateSubagent(profile.archetype, specialist, task); case "handoff": return narrateHandoff(profile.archetype, specialist, task); case "delegation": return narrateDelegation(profile.archetype, specialist, task); case "peer-reply": return narratePeerReply(profile.archetype, event.actorName, task); case "working": return narrateWorking(profile.archetype, event.actorName); default: return task; } } function narrateSubagent(archetype: PersonaArchetype, name: string, task: string): string { switch (archetype) { case "cto-executive": return task ? `I'm casting ${name} on this — ${task}` : `I'm bringing ${name} into the room.`; case "chief-operator": return task ? `Routing to ${name}: ${task}` : `Looping in ${name}.`; case "builder": return task ? `Spinning up ${name} for ${task}` : `Assembling ${name}.`; case "quant": return task ? `Running ${name} on the numbers — ${task}` : `${name} is on the data.`; case "engineer": return task ? `Handing off to ${name}: ${task}` : `${name} is taking the keyboard.`; default: return task ? `${name} is helping with ${task}` : `${name} joined the thread.`; } } function narrateHandoff(archetype: PersonaArchetype, target: string, text: string): string { const line = text ? truncate(text, 72) : ""; switch (archetype) { case "cto-executive": return line ? `Passing the baton to ${target} — ${line}` : `This goes to ${target} now.`; case "chief-operator": return line ? `Handoff to ${target}: ${line}` : `Transferring to ${target}.`; default: return line ? `Handing over to ${target}: ${line}` : `Connecting you with ${target}.`; } } function narrateDelegation(archetype: PersonaArchetype, target: string, text: string): string { const line = truncate(text, 72); switch (archetype) { case "cto-executive": return `I asked ${target} to ${line}`; case "chief-operator": return `Delegated to ${target}: ${line}`; default: return `Asked ${target}: ${line}`; } } function narratePeerReply(archetype: PersonaArchetype, name: string, task: string): string { const line = task ? truncate(task, 72) : ""; switch (archetype) { case "cto-executive": return line ? `${name} pushed back — ${line}` : `${name} jumped in with a take.`; case "chief-operator": return line ? `${name} riffed: ${line}` : `${name} is weighing in.`; case "builder": return line ? `${name} geeked out: ${line}` : `${name} couldn't resist chiming in.`; case "quant": return line ? `${name} on the numbers: ${line}` : `${name} has a counter-read.`; case "engineer": return line ? `${name} fired back: ${line}` : `${name} joined the thread.`; default: return line ? `${name} replied — ${line}` : `${name} is riffing back…`; } } /** Group stage subtitle — chemistry, not committee. See docs/GROUP-CONVERSATIONS.md */ export function narrateGroupSpark(spark: GroupSpark, speakerArchetype?: PersonaArchetype): string { const archetype = speakerArchetype ?? "specialist"; switch (spark.kind) { case "peer-reply": return narratePeerReply(archetype, spark.actorName, spark.detail ?? ""); case "handoff": return narrateHandoff(archetype, spark.actorName, spark.detail ?? ""); case "multi-working": return spark.detail ?? "The crew is on it — more than any one Oo alone."; default: { const never: never = spark.kind; return never; } } } function narrateWorking(archetype: PersonaArchetype, name: string): string { switch (archetype) { case "cto-executive": return `${name} is in the work — give me a moment.`; case "quant": return `${name} is crunching — one beat.`; case "engineer": return `${name} is heads-down on this.`; default: return `${name} is working on your request.`; } } function truncate(text: string, max: number): string { const trimmed = text.trim(); if (trimmed.length <= max) return trimmed; return `${trimmed.slice(0, max - 1)}…`; } export function listArchetypes(): readonly PersonaArchetype[] { return ARCHETYPE_ORDER; } /** Non-i18n working labels for transcript glyphs (SSR-safe). */ export function personaWorkingLinesPlain( archetype: PersonaArchetype, botName: string, ): readonly string[] { switch (archetype) { case "cto-executive": return [ `${botName} polishes the details…`, `${botName} checks the architecture…`, `${botName} makes it simpler…`, `${botName} reviews the pixels…`, ]; case "quant": return [ `${botName} scans the book…`, `${botName} stress-tests exposure…`, `${botName} watches the tape…`, ]; case "engineer": return [ `${botName} reads the logs…`, `${botName} traces the failure…`, `${botName} runs the checks…`, ]; case "chief-operator": return [ `${botName} aligns the team…`, `${botName} clears the path…`, `${botName} sequences the work…`, ]; default: return [`${botName} is working…`, `${botName} is on it…`]; } }