Skip to content

Commit 4ebe078

Browse files
authored
chore(release): v0.4.21 (#47)
Promote dev → main and cut v0.4.21 (stylized 0.4.2-1). Includes #44 output-filter, #45 readme, #46 tip-aware streaming reply, dev-track GHCR tags, and the 0.4.21-dev → 0.4.21 version/openapi/README bump.
1 parent c4c0971 commit 4ebe078

20 files changed

Lines changed: 2488 additions & 120 deletions

File tree

.github/workflows/release-docker.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,15 @@ jobs:
2626
else
2727
ref="${{ github.ref_name }}"
2828
fi
29+
version="${ref#v}"
2930
echo "ref=$ref" >> "$GITHUB_OUTPUT"
30-
echo "version=${ref#v}" >> "$GITHUB_OUTPUT"
31+
echo "version=$version" >> "$GITHUB_OUTPUT"
32+
# Dev cuts (e.g. v0.4.21-dev) move :latest-dev and never touch :latest;
33+
# stable cuts move :latest.
34+
case "$version" in
35+
*-dev*) echo "moving=latest-dev" >> "$GITHUB_OUTPUT" ;;
36+
*) echo "moving=latest" >> "$GITHUB_OUTPUT" ;;
37+
esac
3138
3239
- uses: actions/checkout@v4
3340
with:
@@ -51,7 +58,7 @@ jobs:
5158
push: true
5259
tags: |
5360
ghcr.io/etherfunlab/eros-engine:${{ steps.ref.outputs.version }}
54-
ghcr.io/etherfunlab/eros-engine:latest
61+
ghcr.io/etherfunlab/eros-engine:${{ steps.ref.outputs.moving }}
5562
cache-from: type=gha
5663
cache-to: type=gha,mode=max
5764
labels: |

Cargo.lock

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ resolver = "2"
33
members = ["crates/*"]
44

55
[workspace.package]
6-
version = "0.4.20"
6+
version = "0.4.21"
77
edition = "2021"
88
license = "AGPL-3.0-only"
99
repository = "https://github.com/etherfunlab/eros-engine"

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,10 @@ Each chat session carries a six-dimensional relationship vector. The six axes
5252
| `patience` | 0.0 to 1.0 | Tolerance for low-effort or repeated messages. |
5353
| `tension` | 0.0 to 1.0 | Push-pull, friction, and playful resistance. |
5454

55-
Two **composite scores** summarize this vector for prompt-shaping — each is the mean of a disjoint triplet of axes (`warmth` is rescaled to 01 first):
55+
Two **composite scores** summarize this vector for prompt-shaping — each is the mean of a disjoint triplet of axes (`warmth` is rescaled to 0-1 first):
5656

57-
- **bond** (朋友感, how close it feels) = mean(`warmth`, `intimacy`, `tension`)
58-
- **chemistry** (来电感, how charged it feels) = mean(`trust`, `intrigue`, `patience`)
57+
- **bond** (how close it feels) = mean(`warmth`, `intimacy`, `tension`)
58+
- **chemistry** (how charged it feels) = mean(`trust`, `intrigue`, `patience`)
5959

6060
Updates are smoothed with exponential moving average (EMA), so the persona does not jump between emotional states. `intrigue`, `patience`, and `tension` also decay or recover with real time. The smoothing strength is set by `EMA_INERTIA` (default `0.8`): each turn applies only `1 − inertia` of the evaluated delta, so a higher value makes the relationship build (and cool) more slowly — in effect a difficulty dial — while `0` applies every delta in full.
6161

@@ -133,7 +133,7 @@ eros-engine-llm = "0.4" # only if you want the OpenRouter + Voyage clients
133133
Multi-arch (`linux/amd64`, `linux/arm64`) images for `eros-engine-server` are published to GitHub Container Registry on every `v*` tag:
134134

135135
```bash
136-
docker pull ghcr.io/etherfunlab/eros-engine:0.4.1
136+
docker pull ghcr.io/etherfunlab/eros-engine:0.4.21
137137
# or track the latest tagged release
138138
docker pull ghcr.io/etherfunlab/eros-engine:latest
139139
```
@@ -142,7 +142,7 @@ Minimal run (you bring Postgres + your own `.env`):
142142

143143
```bash
144144
docker run --rm -p 8080:8080 --env-file .env \
145-
ghcr.io/etherfunlab/eros-engine:0.4.1 serve
145+
ghcr.io/etherfunlab/eros-engine:0.4.21 serve
146146
```
147147

148148
The `docker/Dockerfile` is the same artifact used to build this image. Deploy it on any container host.

crates/eros-engine-core/src/pde.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,27 @@ const GHOST_DELTA_TENSION: f64 = 0.05;
3232
///
3333
/// Phase 2: rules only. Phase 6 adds the LLM fallback path.
3434
pub fn decide(input: &DecisionInput) -> ActionPlan {
35+
// 0. Tip on a user message — always reply, never ghost. Tone is driven by
36+
// tip_personality (injected into the prompt downstream); the ReplyStyle
37+
// here is only a baseline / fallback. Affinity deltas stay normal.
38+
if let Event::UserMessage {
39+
tips_amount_usd: Some(_),
40+
..
41+
} = &input.event
42+
{
43+
let reply_style = match input.persona.genome.tip_personality.as_deref() {
44+
Some(_) => ReplyStyle::Neutral,
45+
None => ReplyStyle::Tsundere,
46+
};
47+
return ActionPlan {
48+
action_type: ActionType::Reply,
49+
reply_style,
50+
affinity_deltas: predict_reply_deltas(input),
51+
energy_cost: ENERGY_COST_REPLY,
52+
context_hints: vec![],
53+
};
54+
}
55+
3556
// 1. Gift event — deterministic
3657
if matches!(input.event, Event::Gift { .. }) {
3758
return ActionPlan {
@@ -211,6 +232,20 @@ mod tests {
211232
tier: None,
212233
memory_scope: Default::default(),
213234
affinity_scope: Default::default(),
235+
tips_amount_usd: None,
236+
}
237+
}
238+
239+
fn tip_msg(amount: f64) -> Event {
240+
Event::UserMessage {
241+
content: String::new(),
242+
message_id: Uuid::new_v4(),
243+
prompt_traits: Vec::new(),
244+
audit: None,
245+
tier: None,
246+
memory_scope: Default::default(),
247+
affinity_scope: Default::default(),
248+
tips_amount_usd: Some(amount),
214249
}
215250
}
216251

@@ -236,6 +271,53 @@ mod tests {
236271
assert_eq!(plan.reply_style, ReplyStyle::Excited);
237272
}
238273

274+
#[test]
275+
fn test_tip_forces_reply_even_when_ghost_signals_present() {
276+
// Same affinity that drives test_ghost_threshold_triggers_ghost_action.
277+
let mut affinity = base_affinity();
278+
affinity.intrigue = 0.05;
279+
affinity.patience = 0.05;
280+
affinity.tension = 0.5;
281+
let input = DecisionInput {
282+
event: tip_msg(20.0),
283+
affinity,
284+
persona: persona_with_tip(None),
285+
signals: base_signals(),
286+
};
287+
let plan = decide(&input);
288+
assert_eq!(
289+
plan.action_type,
290+
ActionType::Reply,
291+
"a tip must never be ghosted"
292+
);
293+
}
294+
295+
#[test]
296+
fn test_tip_reply_style_neutral_when_personality_present() {
297+
let input = DecisionInput {
298+
event: tip_msg(20.0),
299+
affinity: base_affinity(),
300+
persona: persona_with_tip(Some("傲娇")),
301+
signals: base_signals(),
302+
};
303+
let plan = decide(&input);
304+
assert_eq!(plan.action_type, ActionType::Reply);
305+
assert_eq!(plan.reply_style, ReplyStyle::Neutral);
306+
}
307+
308+
#[test]
309+
fn test_tip_reply_style_tsundere_when_personality_absent() {
310+
let input = DecisionInput {
311+
event: tip_msg(20.0),
312+
affinity: base_affinity(),
313+
persona: persona_with_tip(None),
314+
signals: base_signals(),
315+
};
316+
let plan = decide(&input);
317+
assert_eq!(plan.action_type, ActionType::Reply);
318+
assert_eq!(plan.reply_style, ReplyStyle::Tsundere);
319+
}
320+
239321
#[test]
240322
fn test_ghost_threshold_triggers_ghost_action() {
241323
let mut affinity = base_affinity();

crates/eros-engine-core/src/types.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ pub enum Event {
6363
/// Defaults to `bond` when absent.
6464
#[serde(default)]
6565
affinity_scope: AffinityScope,
66+
/// Optional caller-supplied tip amount in USD. When `Some`, this turn
67+
/// is a tip: the PDE forces a reply (never ghost) and the reply prompt
68+
/// gets a tip fragment. `None` for normal messages.
69+
#[serde(default)]
70+
tips_amount_usd: Option<f64>,
6671
},
6772
Gift {
6873
gift_id: Uuid,
@@ -224,6 +229,23 @@ mod tests {
224229
}
225230
}
226231

232+
#[test]
233+
fn event_user_message_defaults_tips_amount_to_none() {
234+
let raw = r#"{"UserMessage":{"content":"hi","message_id":"00000000-0000-0000-0000-000000000001"}}"#;
235+
let ev: Event = serde_json::from_str(raw).expect("legacy body deserialises");
236+
match ev {
237+
Event::UserMessage {
238+
tips_amount_usd, ..
239+
} => {
240+
assert!(
241+
tips_amount_usd.is_none(),
242+
"missing field must default to None"
243+
);
244+
}
245+
_ => panic!("expected UserMessage"),
246+
}
247+
}
248+
227249
#[test]
228250
fn chat_response_defaults_audit_fields_to_none() {
229251
let r = ChatResponse {

crates/eros-engine-llm/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ keywords = ["companion", "openrouter", "voyage", "embeddings", "llm"]
1212
categories = ["api-bindings"]
1313

1414
[dependencies]
15-
eros-engine-core = { path = "../eros-engine-core", version = "0.4.20" }
15+
eros-engine-core = { path = "../eros-engine-core", version = "0.4.21" }
1616
serde = { workspace = true }
1717
serde_json = { workspace = true }
1818
reqwest = { workspace = true }

0 commit comments

Comments
 (0)