From 0cf30c5295866e09d06dabc1df49cf8ec16534e1 Mon Sep 17 00:00:00 2001 From: Shaikh Taimoor <60020024+staimoorulhassan@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:45:01 +0500 Subject: [PATCH] feat(mascot): add emotional reaction layer for conversation state (#1144) (#3019) Co-authored-by: Taimoor Co-authored-by: Steven Enamakel --- app/src/features/human/useHumanMascot.test.ts | 29 +++++++++++++++++++ app/src/features/human/useHumanMascot.ts | 7 +++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/app/src/features/human/useHumanMascot.test.ts b/app/src/features/human/useHumanMascot.test.ts index e577dc981..a3e0c95c5 100644 --- a/app/src/features/human/useHumanMascot.test.ts +++ b/app/src/features/human/useHumanMascot.test.ts @@ -572,6 +572,35 @@ describe('useHumanMascot state machine', () => { expect(result.current.face).toBe('concerned'); }); + it('shows concerned on chat_done when a tool succeeded but a subagent failed in the same turn', () => { + const { result } = renderHook(() => useHumanMascot({ speakReplies: false })); + act(() => { + capturedListeners?.onInferenceStart?.(fakeEvent({})); + capturedListeners?.onToolResult?.( + fakeEvent({ tool_name: 'run', skill_id: 's', output: 'ok', success: true, round: 1 }) + ); + capturedListeners?.onSubagentDone?.( + fakeEvent({ + tool_name: 'researcher', + skill_id: 'sa1', + message: 'failed', + success: false, + round: 1, + }) + ); + capturedListeners?.onDone?.( + fakeEvent({ + full_response: 'Sorry, the researcher failed.', + reaction_emoji: null, + rounds_used: 2, + total_input_tokens: 1, + total_output_tokens: 1, + }) + ); + }); + expect(result.current.face).toBe('concerned'); + }); + it('resets work tracking on each new turn', () => { const { result } = renderHook(() => useHumanMascot({ speakReplies: false })); // Turn 1: tool succeeded → celebrating diff --git a/app/src/features/human/useHumanMascot.ts b/app/src/features/human/useHumanMascot.ts index 693b3a2d6..58a926540 100644 --- a/app/src/features/human/useHumanMascot.ts +++ b/app/src/features/human/useHumanMascot.ts @@ -171,9 +171,9 @@ const HAPPY_TEXT_RE = /\b(done|completed|fixed|success|successful|ready|all set| const PROUD_TEXT_RE = /\b(successfully completed|all tasks? (done|finished)|mission accomplished|everything (works?|is working)|all (checks?|tests?) pass(ed)?)\b/i; const CURIOUS_TEXT_RE = - /\b(interesting|fascinating|curious(ly)?|let me (check|look|investigate)|i('ll)? (look|check) into|actually|turns? out)\b/i; + /\b(interesting|fascinating|curious(ly)?|let me (check|look|investigate)|i('ll)? (look|check) into|turns? out to be)\b/i; const CAUTIOUS_TEXT_RE = - /\b(be careful|warning|caution|heads? up|please note|make sure|important(ly)?|note that|worth (noting|mentioning))\b/i; + /\b(be careful|warning|caution|heads? up|please note|make sure|important to note|note that|worth (noting|mentioning))\b/i; const CELEBRATING_TEXT_RE = /\b(congrat(ulations|s)?|well done|bravo|hooray|woohoo|amazing|fantastic|incredible|awesome work)\b/i; const GREETING_TEXT_RE = @@ -200,6 +200,9 @@ export function pickConversationAckFace(event: ConversationAckEvent): Conversati const text = event.full_response?.trim() ?? ''; if (!text) return null; + // Priority: concerned > cautious > proud > confused > curious > happy. + // Concerned and cautious share some vocabulary; check concerned first so + // outright failures don't get softened to a heads-up. if (CONCERNED_TEXT_RE.test(text)) return 'concerned'; if (CAUTIOUS_TEXT_RE.test(text)) return 'cautious'; if (CELEBRATING_TEXT_RE.test(text)) return 'celebrating';