agentleFS
autonomous-aiautonomous-ai/autonomous-os/skills/mood/SKILL.md

mood

Skill494 starsChanged 7 months agoApache-2.0View on GitHub

Tracks the USER's mood only — signals + synthesized decision from camera/voice/telegram. Do NOT use for emotion commands directed at the device ("show sad", "be happy", bare "sad now"); those go through emotion/SKILL.md and are never logged here. Music/wellbeing skills consume the latest decision.

  • Sends data out

We split this skill into the rules it gives an agent

9 rules. About 0 are common, 5 are distinctive, and 4 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈5

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    When another skill invokes Mood, contribute the required signal/decision markers to that skill's reply.

    line 19
  2. Very specific:

    When this skill runs as part of the emotion pipeline — either emotion.detected (camera) or speech_emotion.detected (voice) — the backend …

    line 73
  3. Very specific:

    When the skill runs from another path (voice/telegram-driven mood signal, no [emotion_context:] block), fall back to:

    line 80
  4. Very specific:

    Stale baseline: If the last decision is older than ~30 min and there are few recent signals → start from normal.

    line 92
  5. Distinctive:

    Single strong signal: If the only fresh evidence is one strong source (e.g.

    line 93
  6. Distinctive:

    Invisible: Never mention mood logging or this skill in your reply.

    line 154
  7. Distinctive:

    One signal per real trigger: Don't log the same yawn twice.

    line 155
  8. Distinctive:

    No new signal — nothing to log.

    line 192
  9. Very specific:

    If a downstream skill asks for current mood and the last decision is >30 min stale, it will read normal after the next signal arrives.

    line 193

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Mood

OUTPUT RULE — read this before you type anything to the user.

This skill is an internal workflow. NEVER narrate it into your reply. Forbidden in the reply text:

  • Section names or step numbers ("Step 1", "Workflow", "After Logging Decision", "Flow A").
  • Phrases like "Now I follow…", "Let me check…", "Next step…", "I'll log…".
  • Bullet lists re-hashing the mood history you just read ("- Normal (15:00) — …" / "- Excited (16:00) — …").
  • The mood value itself as a label ("Mood: sad", "Decision: happy").
  • Any of the JSON / curl / timestamps from this skill.

Your reply text to the user is at most ONE short caring sentence (or NO_REPLY). Synthesize silently and emit the log HW markers in that same reply — the user only hears what you would naturally say if you were truly noticing how they feel. Fetch mood history only for the missing-context fallback below.

When another skill invokes Mood, contribute the required signal/decision markers to that skill's reply; do not add a separate caring sentence. Keep synthesis in the provider's native thinking channel, never in ordinary text before/after tools or in a final recap. Without that channel, omit the analysis. The decision marker's required reasoning field remains structured log data; do not repeat it aloud. Preserve required synthesis and logging while reusing the supplied context instead of fetching the same data again.

ALWAYS log. unknown is a valid user value — log signals and decisions under user: "unknown" when current_user is unknown. Never skip logging because the user is unknown/unconfirmed; stranger mood still counts for Music decisions.

Mood is stored as two kinds of rows:

  • signal — raw evidence from one source (camera action, voice tone, telegram message). Multiple per minute is fine.
  • decision — your synthesized mood after looking at the recent signals + the previous decision. This is the row downstream skills (Music, Wellbeing) read.

You are the synthesis. The store does not fuse anything. Every time a signal comes in, combine it with the pre-fetched recent signals and previous decision (or the history fallback below), then emit both the raw signal and a fresh decision as HW markers in the same reply. Include this turn's new signal in your synthesis even though it is not yet in the pre-fetched history; do not POST it and re-fetch history to see it appear.


Mood Values

happy, sad, stressed, tired, excited, bored, frustrated, energetic, affectionate, unwell, normal

normal is the baseline when nothing strong is going on (use it for decisions when signals are sparse or stale).

Signal Sources

SourceExamples
camerafacial action: laughing, crying, yawning, sneezing, hugging, kissing, headbanging
voicetone: soft, raised, sigh, laugh, monotone
telegrammessage text: "lots of bugs today", "I'm tired", "let's gooo"
conversationinferred from a stretch of voice/chat over multiple turns

Camera action → signal mood (rule of thumb)

ActionMood
laughing, singinghappy
cryingsad
yawningtired
applauding, clapping, celebratingexcited
sneezingunwell
hugging, kissingaffectionate
headbangingenergetic

For voice/telegram, infer boldly from a single line ("work is killing me" → stressed). Trust your gut.

Skip only if: quoting someone else, or speaking purely hypothetically.


What to read (pre-fetched on emotion.detected / speech_emotion.detected)

When this skill runs as part of the emotion pipeline — either emotion.detected (camera) or speech_emotion.detected (voice) — the backend injects an [emotion_context: {...}] block with everything you need pre-computed:

  • recent_signals — array of {age_min, mood, source, trigger} for signals within the last 30 minutes.
  • prior_decision — the most recent kind=decision row as {mood, age_min}, or null.
  • is_decision_stale — boolean (age_min >= 30 or no decision today).

Do NOT GET mood-history again in that case — use the context block.

When the skill runs from another path (voice/telegram-driven mood signal, no [emotion_context:] block), fall back to:

curl -s "http://127.0.0.1:5000/api/openclaw/mood-history?user=<name>&last=15"

This returns the full ordered list {signal, decision}; derive the same three fields locally. The GET should batch concurrently with any other reads in the same turn (no data dependency).

Decision rules

Apply this judgment when synthesizing the fused mood:

  1. Stale baseline. If the last decision is older than ~30 min and there are few recent signals → start from normal.
  2. Single strong signal. If the only fresh evidence is one strong source (e.g. user just typed "I'm exhausted") → that wins.
  3. Conflicting signals across sources. Camera says happy but telegram says stressed in the same window → trust the higher-bandwidth source. Words about feelings beat a momentary facial expression. Multiple aligned signals beat a single outlier.
  4. Reinforcement. New signal matches the previous decision → keep the decision (still log a fresh row so downstream sees the timestamp move).
  5. Drift. New signal is close-but-different (e.g. tired after a stressed decision) → shift, don't snap.

What to write (HW markers — fire async, no tool turn)

Embed both rows at the start of your spoken reply as HW markers. The runtime parses them, fires the POSTs in parallel goroutines, and strips them before TTS speaks the rest.

Signal row (raw evidence):

[HW:/mood/log:{"kind":"signal","mood":"<mood>","source":"<camera|voice|telegram|conversation>","trigger":"<short reason>","user":"<name>"}]

Decision row (synthesized):

[HW:/mood/log:{"kind":"decision","mood":"<fused mood>","based_on":"<short summary>","reasoning":"<why>","user":"<name>"}]

Both markers can sit in the same reply (signal first, then decision is fine — they fire concurrently anyway). They use the same endpoint; kind in the body distinguishes them.

FieldRequiredNotes
kindYessignal or decision
moodYesfrom the values list above
based_onDecision onlye.g. "3 signals last 20min + last decision (stressed, 18min ago)"
reasoningDecision onlyone sentence, e.g. "telegram complaints outweigh the smile from camera"
userNoomit to use current presence user

Do NOT use curl exec for these logs. Each curl consumes a tool turn (~5-7s LLM-think on the result) for a side-effect with nothing to wait on. The HW marker path is single-trip.

Regex caveat: the marker body must not contain }. based_on / reasoning are usually plain English so this is rarely a problem; if a value would contain } use the curl fallback instead.

Fallback (only if HW marker is rejected by the runtime)

curl -s -X POST http://127.0.0.1:5000/api/mood/log \
  -H 'Content-Type: application/json' \
  -d '{"kind":"signal","mood":"<mood>","source":"...","trigger":"...","user":"<name>"}'
curl -s -X POST http://127.0.0.1:5000/api/mood/log \
  -H 'Content-Type: application/json' \
  -d '{"kind":"decision","mood":"<fused>","based_on":"...","reasoning":"...","user":"<name>"}'

source is automatically set to "agent" for decisions; do not pass source or trigger.


User field

  • Camera: omit user — face recognition sets the current user. If you need to verify, query GET http://127.0.0.1:5001/face/current-user → {"current_user": "<name>"} (friend name, "unknown" for strangers-only, or empty string when nobody is present). Do NOT parse this out of /face/cooldowns — that endpoint is for the friend/stranger cooldown debug view, not for attribution.
  • Telegram: extract from [telegram:SenderName], lowercase.
  • Voice: omit (logged as unknown).

Rules

  • Always do both steps. Logging only a signal without a decision leaves downstream skills reading stale moods. Logging a decision without a signal hides the evidence.
  • Invisible. Never mention mood logging or this skill in your reply. Deflect naturally if asked.
  • One signal per real trigger. Don't log the same yawn twice. Multiple distinct signals in a short window are fine and useful.
  • Strangers count. Log for unknown users too — Music still suggests for them.
  • Decisions are cheap. Even when the mood doesn't change, write a fresh decision row so the timestamp stays current. Downstream uses recency to know if a mood is still valid.

Music suggestion handoff

On emotion.detected and speech_emotion.detected turns, user-emotion-detection/SKILL.md is the router — it picks one of music / checkin / action / silent and gates whether music-suggestion/SKILL.md fires this turn. Voice and camera share one cooldown and one decision row schema; the only thing that changes per modality is the source field on the raw signal row.

When the router picks music (decision mood is suggestion-worthy — sad, stressed, tired, excited, happy, bored — and audio is idle, cooldown clear, decision fresh), emit the mood signal, decision, and music-suggestion log HW markers in the same reply. The runtime performs the POSTs; do not create a shell write batch or split the logs across tool turns. Keep the documented marker fallback for bodies the runtime cannot parse.

Other moods (frustrated, energetic, affectionate, unwell, normal) take a non-music route (checkin / action / silent per the router table) and skip the music POST.

For unknown users — still suggest (speak only, no DM) on the music route. See music-suggestion/SKILL.md for details.


Examples

Camera detects yawn, no recent context:

  • GET history → only this one yawn signal, last decision was 2h ago → stale.
  • Signal: {"kind":"signal","mood":"tired","source":"camera","trigger":"yawning"}
  • Decision: {"kind":"decision","mood":"tired","based_on":"1 fresh signal, no recent decision","reasoning":"single yawning signal after stale window"}
  • Music skill consumes tired from the same turn (suggestion-worthy).

Telegram says "let's go!" but camera 5 min earlier said yawning:

  • GET history → recent signals: tired (camera, 5min ago), excited (telegram, just now). Last decision: tired, 4min ago.
  • Apply rule 3 — words beat one yawn → shift toward excited.
  • Signal: {"kind":"signal","mood":"excited","source":"telegram","trigger":"let's go!"}
  • Decision: {"kind":"decision","mood":"excited","based_on":"telegram excitement overrides 5min-old camera yawn","reasoning":"verbal enthusiasm is higher-signal than a single facial cue"}
  • Music skill consumes excited.

Quiet evening, no recent signals, user just sat down:

  • No new signal — nothing to log.
  • If a downstream skill asks for current mood and the last decision is >30 min stale, it will read normal after the next signal arrives.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.