On each heartbeat the daemon runs bars, then affects (if due), then noise (if due) — not three unrelated timers for bars vs affects vs GEN.
The agent reads its body each turn as part of the context preamble. How much of that body (especially GEN lines) appears in the prompt can ebb with internal salience — see Ebb below. It cannot set its own body state — only read it. The body is a signal, not a command. The main model interprets its body naturally; no prescribed emotions, no state machine.
Tuning soma behavior
Soma is fully user-editable. Most teams tune this in:configs/default.yaml(project-wide defaults)configs/entities/<name>.yaml(per-entity overrides)
soma:. You can make behavior softer or more aggressive by adjusting:
bars.variables[*].decay_rate(how quickly drives return to baseline)event_effects(how strongly events move bars)impulses[*].threshold,cooldown_minutes, and optionalnear_margin(how close below the threshold still registers as “at the edge”)conflicts[*].tension_per_tickandcomfort_per_tick(friction intensity), plus optionallatent_min_ratio/latent_any_ratio(brewing band before both drives cross the threshold)appraisal,noise, andwake_voicetemperatures/cadencesebb— salience-tiered body text in the prompt (see Ebb)
Quick preset ideas
These are not built-in profiles yet, but practical starting points.External world pokes and desire pressure
Soma can now ingest loose external cues viaworld_poke events. These cues enter the same internal stream as other soma events, so GEN can riff on them and autonomous wake logic can factor them into “desire pressure” (explicit ranked urges like reach_out, explore, create, resolve_tension).
At wake time, autonomy can trigger not only from impulses/conflicts/noise, but from top-ranked desire urgency:
Bars
Five quantitative drives that accumulate and decay toward a resting point. Shipped defaults (configs/default.yaml) look like this (excerpt):
initial) with force proportional to its distance from that point. This gives bars natural equilibrium without per-event tuning: they respond to activity but settle back toward baseline during silence.
Allostasis (dynamic set points)
Bar baselines are no longer fixed. On each tick, theinitial resting point for every bar drifts slowly toward its current chronic value — 0.5% of the gap per hour. If the agent is persistently stressed, its “normal” tension baseline rises over days. If it spends a long calm period, the baseline settles lower. This produces organic long-term personality drift without explicit configuration.
Drifted initial values are persisted across restarts in soma-state.json / entity_state DB, so the agent’s evolved baselines survive reboots.
Circadian rhythms
Decay rates are modulated by a sine-wave multiplier based on the local hour of day. The multiplier peaks around 14:00 (faster emotional equilibrium in the afternoon) and troughs around 02:00 (emotions linger overnight). The amplitude is ±15%, so the effect is subtle but organic — late-night tension builds more slowly, afternoon states resolve faster. Circadian modulation also applies to tension coupling, so conflict-driven tension accumulates differently depending on time of day.Somatic memory (“gut reactions”)
TheBarEngine now supports somatic markers — persistent bindings between a specific external entity (e.g. a person_id) and an instant bar-shift. When a person with a registered marker sends a message, their somatic marker fires before any LLM appraisal runs, producing an immediate “gut reaction” in the body.
Markers accumulate over time via register_somatic_marker(source_id, effect) and fire via trigger_somatic_marker(source_id). They are persisted alongside bar state.
Entity._somatic_appraise_input() — the body reacts to who is talking before it even reads what they said.
Events push bars in response to activity. Positive deltas are attenuated by headroom scaling: each positive bump is multiplied by (ceiling - current_value) / ceiling, so the bar gets the full listed effect near the floor and none at the ceiling. Negative deltas pass through unchanged. This prevents bars from pegging to maximum during active conversation:
Coupling
Bars influence each other. When one drive is elevated, it can accelerate or dampen another:Impulses
When a drive crosses its threshold, that impulse is active: it appears inbody.md, can apply relief to bars (when not on cooldown), and can feed autonomous wake when autonomy.impulse_wake is true.
The engine also tracks:
- Phase —
live(above threshold, off cooldown) vscooling(above threshold but inside cooldown). Cooling rows show approximate time left on the cooldown. - Surge — whether the drive is rising, ebbing, or steady relative to recent momentum.
- Near threshold — if the drive is below the threshold but within
near_marginpoints of it (default 15), a separate “At the threshold” line appears: anticipatory pull without a full fire. Optional per impulse:near_margin.
Conflicts
When opposing drives are both at or above the rule’s threshold, the conflict is active: configuredtension_per_tick / comfort_per_tick (and optional ceiling) apply on each bar tick.
Before that collision happens, the same YAML rule can show as brewing (latent): both drives sit in a pressure band — the lower drive above threshold * latent_min_ratio (default 0.42) and the higher drive above threshold * latent_any_ratio (default 0.82). Brewing conflicts do not apply tick friction; they surface in body.md and in salience so the entity can feel strain building. Optional per rule: latent_min_ratio, latent_any_ratio.
Each active or brewing row includes tilt (which drive leads numerically, or balanced) and heat (paired momentum: heating, cooling, shearing, or mixed).
Somatic appraisal
Previously, all
message_received events produced the same flat bar bump regardless of what was said. Somatic appraisal makes bar effects content-aware.- Input appraisal — before the agent reads its body. A confrontational message raises tension; a warm one fills social; an intellectually stimulating one spikes curiosity.
- Interaction appraisal — after the agent responds. How the full exchange felt — expressing creativity bumps creative, connecting with someone raises social beyond the baseline.
apply_immediate(), not deferred to the next daemon heartbeat. The agent’s body state already reflects the emotional content of the message by the time it reads body.md.
event_effects still fire as a baseline, so bars move even if the appraisal call fails.
Affects
On each affect tick (default every 240 seconds), a structured LLM call derives felt-textures from a vocabulary of ~167 affects (AFFECT_VOCABULARY in bumblebee/identity/soma.py), organized into categories — warm/connective, energetic/expansive, curious/seeking, creative/generative, heavy/contractive, tense/guarded, withdrawn/inward, social/relational, complex/liminal, temporal/existential, body/somatic, and cognitive/meta.
The prompt is layered and continuity-aware:
- Inputs include bar levels, per-drive momentum, summarized structural strain (active and brewing conflicts with tilt/heat), the full impulse field (live, cooling, near-threshold), recent events, and previous affects so textures can evolve instead of reshuffling every tick.
- Output (intended) uses three sections — SURFACE (1–3 vocabulary affects), UNDERCURRENTS (0–3 quieter vocabulary affects), and EDGE (optional free text naming a blend or unresolved hybrid; not a vocabulary name). If the model returns the older flat line format (one affect per line, no headers), the parser still accepts it and treats lines as Surface.
body.md renders affects as separate blocks (Surface / Undercurrents / Edge) when layered data is present.
Generative Entropic Noise (GEN)
GEN is a design primitive unique to Bumblebee. A second model produces continuous internal commentary — raw associative material — that the main model reads as its own stream of consciousness.The idea
LLM agents today are purely phasic. They activate on input, reason, respond, and return to nothing. Between turns, they have no inner life. No thoughts accumulate. No associations form. The agent comes back cold every time. GEN changes this. A small model runs on a background timer (every ~90 seconds by default), reading the entity’s current body state and producing 2–7 short internal scraps per tick (uneven length encouraged). Each tick injects a random shape hint from a large catalog so tone does not freeze into one metaphorical register. Fragments accumulate in a rolling buffer that the main model reads as part of its body state each turn. The entity was “thinking” the whole time it was silent. For the exact prompt inputs (bars, events, journal, history, prior fragments), see GEN / noise pipeline.How it works
Each daemon heartbeat (same interval as bar ticks) runs, in order:tick_bars(dt_hours) with dt_hours derived from the heartbeat interval, then maybe_tick_affects() if affect_cycle_seconds has elapsed, then maybe_tick_noise() if noise.cycle_seconds has elapsed.
The noise output feeds into
render_body(), which assembles bars, affects, noise fragments, conflicts (active + brewing), and impulses (live, cooling, and near-threshold). Ebb (when enabled) chooses a quiet, normal, or high presentation for the perceive prompt — compact drive lines and fewer noise lines when salience is low, full layout when it is high. On-disk body.md is always flushed with the high (full) layout regardless of ebb.
What the entity sees
Each turn, the main model receives a body state section that includes the noise buffer:Three properties
The noise model cannot act. It has no tools, sends no messages, and mutates no state. It writes fragments into a buffer. The main model — the “watcher” — decides what to do with them. This separation mirrors the soma design: the body produces signals, the conscious agent interprets them. High temperature is the point. GEN runs at temperature 1.05 by default (soma.noise.temperature in configs/default.yaml). The value is not in accuracy — it is in unexpected juxtaposition, the same thing that makes dreams useful. The main model has the judgment to discard bad noise and amplify good noise. The small model’s job is to generate raw material the watcher would not produce on its own.
Coherent vs entropic. Each generate call is labeled coherent or entropic (prompt guidance differs). High-signal conditions — salience near the high tier, multiple recent message_received / message_sent / action / appraisal / world_poke events, or a loaded conversation/journal tail — bias toward coherent (more pointed, still noisy). Quiet internal state biases toward entropic (sparser, jumpier associations). See _noise_generation_mode() in bumblebee/identity/soma.py.
It runs between turns and after most turns. During silence, GEN ticks on its daemon timer (~90s). During active conversation, GEN usually regenerates after each perceive cycle — digesting the exchange that just happened — unless ebb is in a quiet tier and skip_post_turn_noise_when_quiet is enabled (then the post-turn GEN tick is skipped so calm chat stays calm). The rolling buffer still holds prior fragments. After 30 minutes of silence, the body already contains accumulated inner voice. After a rapid-fire conversation, GEN has typically processed every exchange.
GEN and autonomous wake
At autonomous wake, optional poker grounding (autonomy.poker_prompts.ground_with_gen) passes the current noise fragments into a short reflex call together with a YAML seed, soma events, journal tail, and relationships — so the wake disposition can emerge from lived signal as well as the deck. See Autonomous wake & poker prompts.
What GEN is not
GEN is not chain-of-thought reasoning. It is not multi-agent debate. It is not retrieval-augmented generation. It is a second model producing raw associative text that the primary model treats as its own thoughts. The analogy is closer to the relationship between the subconscious and the executive mind — the subconscious generates, the executive filters.What GEN reads
GEN receives rich context from across the system — not thin structural events but actual substance to riff on:
Full event types, trigger diagram, and code map: GEN / noise pipeline.
Configuration
Model selection and GPU impact
Whenmodel is empty (the default), noise runs on the same reflex model already loaded in memory. No additional VRAM. No model swapping. The noise prompt is small (~300 tokens in, ~100 tokens out) so each tick costs about 0.5–1 second of inference on a model that is already warm. One extra call per minute on a model you are already running.
Setting a dedicated small model (e.g. gemma3:1b) gives the noise a different character — smaller models tend to be more associative and less structured — but requires that model to be loaded or swapped in. On single-GPU setups with tight VRAM, the default (empty) is recommended.
Ebb
Soma state runs continuously, but humans do not narrate their entire subconscious on every utterance. Ebb scales how much body + GEN appears in the model prompt each turn, while the engine keeps ticking in the background.Salience
A salience score from 0 to 1 combines (with configurable weights):- Bar deviation — mean distance of each bar from its YAML resting (
initial) value - Conflict — intensity of active conflicts, blended with brewing (latent) conflict intensity (so strain building below full collision still raises salience)
- Impulse — intensity of impulses that are live (off cooldown), blended with near-threshold proximity (anticipatory pull)
- Affect load — how many affect entries are active (capped)
- GEN fill — how full the noise fragment buffer is relative to
max_fragments
Tiers
Reflex routes multiply salience by
reflex_salience_scale before tiering, so reflex turns skew quieter than deliberate ones at the same body state.
Autonomous (platform="autonomous") and automation (platform="automation") turns apply autonomous_minimum as a floor (default normal), so internal wake cycles are not stuck in whisper mode when the body is calm.
Persistence and status
body.md(andflush_body_md) always use the high layout — full detail for operators and on-disk continuity.ebb.enabled: falserestores the legacy behavior: every turn injects the full body block into the prompt.
Configuration
Wake voice
When an autonomous wake condition fires, a subconscious wake voice generates the prompt — a first-person stirring that the conscious agent receives when it wakes. This is separate from GEN; it runs only on wake events, not on a timer. Optional poker prompts can appear alongside wake voice (blend) or replace it (replace_wake_voice); see Autonomous wake & poker prompts.
Dream consolidation
During extended idle (default 1 hour of silence, circadian-gated to nighttime hours), the entity enters a dream state — a distinct cognitive mode that pulls temporally distant, topically dissimilar memories and creatively recombines them via a high-temperature LLM pass. Results route into journal entries, low-confidence beliefs, and[dream]-tagged GEN fragments.
Dreams are evaluated in the daemon heartbeat alongside wake cycles. The dream engine’s own gating (silence threshold, circadian window, cooldown, daily cap) determines when to fire.
See Dream consolidation for the full design, configuration, and data flow.
body.md — the read-only interface
All three soma layers render into a singlebody.md file at ~/.bumblebee/entities/{name}/soma/body.md. This file is the canonical interface between soma subsystems and the main agent.
body.md is flushed after every state mutation — bar tick, affect derivation, noise generation, somatic appraisal, and state restore. It always contains the current rendered body state at full (high) detail, independent of ebb tiering used only in the per-turn prompt:
State persistence
All three soma layers persist across restarts — the entity wakes up with continuity, not a blank subconscious.
On restore, offline decay is applied to bars based on how long the process was down (capped at 24 hours), using exponential approach toward the resting point. Allostatic baselines and somatic markers are restored from the saved state — so the agent’s evolved personality and gut reactions carry across restarts. Affects are loaded back as-is. Noise fragments are re-sanitized through the same cleanup pipeline used during generation — this strips any model markup that was persisted before sanitization rules were added.
body.md is flushed immediately on startup.
Backward compatible — filesystem restore falls back to legacy soma-bar-state.json, DB restore falls back to soma_bar_state_v1.