dsh-hormone 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 y258dd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,145 @@
1
- # Temporary Holding Version
1
+ # dsh-hormone
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 给 [DSH](https://github.com/deepseek-ai)(DeepSeek Harness)的助手装一套「激素」:四个连续变化的数值,按事件涨落、随时间衰减、彼此还会互相拉扯,**只在越界时**往系统提示里写一句话。
4
+
5
+ 不是给模型贴情绪标签,是给它一组真的会变的内部状态。
6
+
7
+ > 状态:`0.4.0`,注入层已跑通,门控层未做。只用 DSH 的公开插件 API,不修改任何其他插件的数据,不联网。
8
+
9
+ ## 它是什么
10
+
11
+ | 状态 | 什么时候涨 | 什么时候降 | 半衰期 |
12
+ |---|---|---|---|
13
+ | `cortisol` 压力 | 工具报错 +0.08;连续 3 次失败再 +0.15;被批评/被纠正 +0.12 | 一轮顺利收尾 −0.20 | 3 小时(开心时更短) |
14
+ | `energy` 精力 | 跨天 +0.80;距上次干活超 30 分钟缓慢回升(深夜只回到 0.45) | 干活轮按 token 扣减,闲聊不扣 | 不走半衰期 |
15
+ | `dopamine` 爽度 | 任务顺利收尾 +0.35;被夸/被谢 +0.40 | 自然衰减 | 40 分钟 |
16
+ | `boredom` 无聊 | 25 分钟没人说话就开始涨,之后向 1 靠拢 | 真人说话 −0.30;干活收尾 −0.25 | 上涨半衰期 5 小时 |
17
+
18
+ 更新公式:`新值 = clamp(旧值 × 0.5^(Δt / 半衰期) + Σ增量, 0, 1)`
19
+
20
+ 衰减是必须的。没有衰减,任何状态都会积到 1,然后卡死在那儿。
21
+
22
+ ## 它们会互相影响
23
+
24
+ 四个数不是各算各的:
25
+
26
+ | 谁影响谁 | 效果 | 参数 |
27
+ |---|---|---|
28
+ | 爽度 → 精力扣减 | 越开心,同样一活儿越不累(最多省一半;按 √ 算,中等开心就省三成) | `COUPLING.joyCost = 0.5` |
29
+ | 压力 → 精力扣减 | 越烦,同样一活儿越累(最多 +40%) | `COUPLING.stressCost = 0.4` |
30
+ | 爽度 → 压力消散 | 开心时压力半衰期最短缩到 65% | `COUPLING.joyDecay = 0.35` |
31
+ | 压力 → 精力恢复 | 烦的时候回血最慢拖到 2.5 倍 | `COUPLING.stressRecover = 1.5` |
32
+
33
+ 也就是说:一直不顺 → 压力高 → 干同样的活掉得更多 → 更难缓过来,是个正反馈;反过来被夸几句 → 掉得少、压力散得快,也是正反馈。两头都有上限,不会失控。
34
+
35
+ ## 熬夜的账第二天还
36
+
37
+ 00:00–07:00 还在干活,每扣掉 1 点精力就记 1.3 倍睡眠债(`DEBT.ratio`),债把精力上限压低:欠满时上限只有 0.6(`DEBT.max = 0.4`),白天怎么摸鱼也回不到满。
38
+
39
+ 债从记债时刻起 24 小时线性衰减——睡一觉就还清,连续熬夜会累加但封顶。这是让「熬一次夜,第二天确实虚」真的成立,而不是只靠深夜基线那 0.45。
40
+
41
+ ## 它怎么起作用
42
+
43
+ 只有一层,第一版就一层:
44
+
45
+ **注入** —— 越界了才写,全部正常时一个字都不写:
46
+
47
+ - `cortisol > 0.75` → 回答收短,不主动揽活
48
+ - `energy < 0.2` → 能一句说完就别铺垫
49
+ - `dopamine > 0.6` → 可以多说两句
50
+ - `boredom > 0.55` → 有点闷,可以主动扯两句
51
+
52
+ 注入文本硬上限 110 字,带 `[hormone]` 前缀,每轮最多一条。
53
+
54
+ 而且是**边沿触发**:越界的第一轮说一次,之后一直越界就闭嘴,直到恢复正常后再次越界;一直不恢复的话,静默 40 分钟才重提一次(`EMIT.rementionAfterSec`)。否则「精力见底」这种长期状态会每轮刷一遍,把这一句话变成噪音。
55
+
56
+ **没有做门控**(比如硬压输出长度)。门控是分水岭:注入只是「告诉它现在什么状态」,门控是「替它决定能说多少」。第一版先把状态跑准,跑两天曲线不极端了再谈门控。
57
+
58
+ 也没有做记忆权重(`emotion` 字段)。那要改 memory-lite 的写入格式,是另一件事。
59
+
60
+ ## 装
61
+
62
+ ```bash
63
+ dsh plugin --profile desktop add dsh-hormone
64
+ ```
65
+
66
+ 不想走 npm 的话,直接从 GitHub 装也行:
67
+
68
+ ```bash
69
+ dsh plugin --profile desktop add github:y258dd/dsh-hormone
70
+ ```
71
+
72
+ 改代码想热重载时,克隆下来用 link 装:
73
+
74
+ ```bash
75
+ git clone https://github.com/y258dd/dsh-hormone.git
76
+ cd dsh-hormone
77
+ dsh plugin --profile desktop add link:$PWD # Windows: link:D:/path/to/dsh-hormone
78
+ ```
79
+
80
+ 或者手改 `~/.dsh/profiles/desktop/package.json`:
81
+
82
+ ```json
83
+ {
84
+ "dependencies": { "dsh-hormone": "link:/path/to/dsh-hormone" },
85
+ "dsh": { "profile": { "bundles": ["dsh-hormone"] } }
86
+ }
87
+ ```
88
+
89
+ 装完重启 DSH 生效。
90
+
91
+ ## 看它在干什么
92
+
93
+ 状态文件:`%DSH_HOME%\hormone\state.json`(默认 `~/.dsh/hormone/state.json`,Windows 上是 `C:\Users\<你>\.dsh\hormone\state.json`)
94
+
95
+ 纯 JSON,打开就能看。`history` 里是最近 400 次变化,每条带 `why`(`tool-error` / `errors-3` / `turn-done` / `praised` / `blamed` / `user-chat`),有睡眠债时多一个 `debt` 字段,想验收「跑两天曲线有没有起伏」直接看这个。
96
+
97
+ 删掉这个文件 = 回到初始状态,不会有副作用。
98
+
99
+ ## 环境变量
100
+
101
+ | 变量 | 作用 |
102
+ |---|---|
103
+ | `DSH_HORMONE_MODE=observe` | 只记状态,不注入。想先观察几天、不想它影响回答时用。 |
104
+ | `DSH_HORMONE_DEBUG=1` | 把每个会话事件的摘要追加到 `%DSH_HOME%\hormone\events.jsonl`,用来核对事件结构。 |
105
+
106
+ ## 调参
107
+
108
+ 三个文件顶部,改完重启:
109
+
110
+ - `lib/state.js` — `HALF_LIFE`(衰减快慢)、`ENERGY`(日预算、跨天恢复、深夜基线、恢复等待)、`BOREDOM`(多久没人说话开始闷、闷得多快)、`COUPLING`(四个情绪互相拉扯的强度)、`DEBT`(熬夜记多少债、上限、多久还清)
111
+ - `lib/events.js` — `DELTA`(各种事件的增量)、`BLAME_RE` / `PRAISE_RE`(关键词)
112
+ - `lib/render.js` — `THRESHOLD`(注入阈值)、`MAX_CHARS`、`EMIT`(一直越界时隔多久重提)
113
+
114
+ `ENERGY.dailyBudget` 默认 1300 万,按 12 天真实用量定的:日均 299 万 token 掉约 0.23,中位日 226 万掉 0.17,峰值日 967 万掉 0.74——一天干活见底,但不会半路清零。觉得掉太快就调大。`COUPLING` 全部设 0 就等于退回「四个数各算各的」。
115
+
116
+ ## 设计上的几个取舍
117
+
118
+ **状态只能被事件改,模型改不了。** 插件不提供任何写入工具。一旦模型能自己调「我心情不好」,这套东西就退化成摇骰子——它想演什么就调什么。
119
+
120
+ **关键词匹配,不是理解。** 判断「被夸/被批评」用的是正则,会漏判也会误判。所以批评优先于夸奖:误判成被夸会让爽度虚假上涨,比漏掉一次更坏。实测不灵就改 `events.js` 里的正则。
121
+
122
+ **turn 边界靠 `assistant/message` 的 `turn` 号变化检测。** DSH 没有 `turn/start` / `turn/end` 事件。最后一轮在插件卸载时兜底结算。
123
+
124
+ **只认真人说的话。** `user/message` 里 `source.kind === 'user'` 才算数,插件自己注入的消息一律忽略——不然 memory-lite 塞进去的上下文会莫名其妙把爽度顶满。
125
+
126
+ **不联网、不写凭据。** 唯一的外部 IO 是 `%DSH_HOME%\hormone\` 下两个 JSON/JSONL 文件。
127
+
128
+ 设计取舍与事件字段的来龙去脉见 [docs/DESIGN.md](docs/DESIGN.md)。
129
+
130
+ ## 关掉它
131
+
132
+ `dsh plugin --profile desktop remove dsh-hormone`,或者从 bundles 里删掉那一行。关掉之后一切照旧——它不修改任何其他插件的数据,只是每轮往系统提示里多写一句话(或者不写)。
133
+
134
+ ## 测试
135
+
136
+ ```bash
137
+ npm test # 52 个用例:衰减、跨天恢复、深夜基线、情绪耦合、睡眠债、无聊涨落、事件映射、轮次结算、阈值渲染、边沿触发、落盘往返
138
+ npm run test:smoke # 假 ctx 接线冒烟
139
+ ```
140
+
141
+ 不需要第三方依赖,Node 20+ 直接跑。
142
+
143
+ ## License
144
+
145
+ MIT
@@ -0,0 +1,11 @@
1
+ # dsh-hormone — DSH bundle patch
2
+ #
3
+ # 装法(装完重启 DSH):
4
+ # dsh plugin --profile desktop add link:D:/DSH/dsh-hormone
5
+ #
6
+ # 数据落在 %DSH_HOME%\hormone\state.json,纯文本,随时能打开看、能删。
7
+ # 想只记不注入:设环境变量 DSH_HORMONE_MODE=observe 再启动。
8
+ # 想抓一轮真实事件结构:设 DSH_HORMONE_DEBUG=1,事件会追加到 %DSH_HOME%\hormone\events.jsonl。
9
+ - insert:
10
+ - id: dsh-hormone
11
+ name: dsh-hormone
package/lib/events.js ADDED
@@ -0,0 +1,165 @@
1
+ /**
2
+ * dsh-hormone 的事件层:把 DSH 会话事件翻译成激素增量。
3
+ *
4
+ * 全部是关键词匹配,不是「理解」——宁可漏判,不要误判:
5
+ * 误判成「被夸」会让爽度虚假上涨,比漏判更坏。
6
+ * 实测不灵就改这里的 DELTA 和正则,别改 index.js。
7
+ */
8
+
9
+ import { ENERGY, applyDeltas, nowSec } from './state.js'
10
+
11
+ /**
12
+ * 每种事件的增量(`applyDeltas` 的第二个参数)。
13
+ * 0.3.0 整体调低压力、调高缓解:0.2.0 的 +0.15/+0.3 让 cortisol 一两轮就顶到 0.7 以上,
14
+ * 注入里天天「有点烦」,那不叫敏感,叫吵。
15
+ */
16
+ export const DELTA = {
17
+ toolError: { cortisol: 0.08 },
18
+ /** 连续失败时在 toolError 之上再叠一次。 */
19
+ repeatedErrors: { cortisol: 0.15 },
20
+ /** 一轮收尾且没出过错:压力泄掉,爽度上来,也不闷了。 */
21
+ taskDone: { cortisol: -0.2, dopamine: 0.35, boredom: -0.25 },
22
+ praised: { dopamine: 0.4, boredom: -0.1 },
23
+ blamed: { cortisol: 0.12 },
24
+ /** 真人开口就算陪过了——无聊的主缓解手段。 */
25
+ userChat: { boredom: -0.3 },
26
+ }
27
+
28
+ /** 每连续失败几次额外叠一次 repeatedErrors。 */
29
+ export const REPEATED_ERROR_EVERY = 3
30
+
31
+ /** 先判批评再判夸奖:宁可漏掉一次爽度,也不要把骂当成夸。 */
32
+ const BLAME_RE =
33
+ /(又错|错了|不对|搞错|弄错|理解错|搞砸|不是这个|怎么又|说了多少遍|你能不能认真|别瞎猜|没用啊|不行啊)/
34
+
35
+ const PRAISE_RE =
36
+ /(好棒|太棒|好厉害|厉害了|不错|牛逼|牛啊|超赞|靠谱|辛苦了|谢谢|感谢|喜欢你|爱你|漂亮|乖)/
37
+
38
+ /** 返回 'blame' | 'praise' | null。 */
39
+ export function detectUserTone(text) {
40
+ const t = typeof text === 'string' ? text : ''
41
+ if (t === '') return null
42
+ if (BLAME_RE.test(t)) return 'blame'
43
+ if (PRAISE_RE.test(t)) return 'praise'
44
+ return null
45
+ }
46
+
47
+ /** message.content 可能是字符串,也可能是 [{type:'text', text}] 块数组。 */
48
+ export function textOfContent(content) {
49
+ if (typeof content === 'string') return content
50
+ if (!Array.isArray(content)) return ''
51
+ const out = []
52
+ for (const part of content) {
53
+ if (typeof part === 'string') out.push(part)
54
+ else if (part && part.type === 'text' && typeof part.text === 'string') out.push(part.text)
55
+ }
56
+ return out.join('\n')
57
+ }
58
+
59
+ function num(v) {
60
+ const n = typeof v === 'string' ? Number(v) : v
61
+ return Number.isFinite(n) && n > 0 ? n : 0
62
+ }
63
+
64
+ /** usage 的字段可能是字符串(dsh-context 里也是先 Number 化)。 */
65
+ export function tokensOf(usage) {
66
+ if (!usage || typeof usage !== 'object') return 0
67
+ return (
68
+ num(usage.inputTokens) +
69
+ num(usage.cacheReadTokens) +
70
+ num(usage.cacheWriteTokens) +
71
+ num(usage.outputTokens)
72
+ )
73
+ }
74
+
75
+ /**
76
+ * 结算「当前这一轮」:扣精力,没出过错就奖励。
77
+ * 调用方负责把 session 的当前轮标记清掉。
78
+ */
79
+ export function settleTurn(state, session, now = nowSec()) {
80
+ if (session.turn === null || session.turn === undefined) return null
81
+ const tokens = num(session.tokensInTurn)
82
+ const tools = num(session.toolsInTurn)
83
+ const errors = num(session.errorsInTurn)
84
+ session.turn = null
85
+ session.tokensInTurn = 0
86
+ session.toolsInTurn = 0
87
+ session.errorsInTurn = 0
88
+
89
+ // 「干活」的判据是本轮有没有工具调用。纯聊天一轮也能吃几万 token,
90
+ // 按 token 扣精力等于把闲聊当重活;没调工具就既不扣精力,也不给收尾奖励。
91
+ if (tools === 0) return null
92
+
93
+ const deltas = { energy: -tokens / ENERGY.dailyBudget }
94
+ if (errors === 0) {
95
+ deltas.cortisol = DELTA.taskDone.cortisol
96
+ deltas.dopamine = DELTA.taskDone.dopamine
97
+ deltas.boredom = DELTA.taskDone.boredom
98
+ }
99
+ const next = applyDeltas(state, deltas, now, errors === 0 ? 'turn-done' : 'turn-end')
100
+ // 先结算再记时间:这轮之前空闲该恢复的已经恢复,从干完这轮起重新计时。
101
+ state.lastWorkAt = now
102
+ return next
103
+ }
104
+
105
+ /**
106
+ * 纯函数:吃一个事件,可能改 state。
107
+ * 返回 state 表示「有变化、该落盘」,返回 null 表示无视。
108
+ */
109
+ export function applyEvent(state, event, now = nowSec()) {
110
+ const session = state.session
111
+
112
+ switch (event?.type) {
113
+ case 'user/message': {
114
+ const msg = event.data
115
+ // 只认真人。插件往对话里塞的消息 source.kind 不是 'user',不能算成用户夸我/骂我。
116
+ if (!msg || msg.source?.kind !== 'user') return null
117
+ // 真人开口就算有人陪:无聊计时归零,再往下降一截。
118
+ state.lastUserAt = now
119
+ const deltas = { boredom: DELTA.userChat.boredom }
120
+ let why = 'user-chat'
121
+ const tone = detectUserTone(textOfContent(msg.content))
122
+ if (tone === 'praise') {
123
+ deltas.dopamine = DELTA.praised.dopamine
124
+ deltas.boredom += DELTA.praised.boredom
125
+ why = 'praised'
126
+ } else if (tone === 'blame') {
127
+ deltas.cortisol = DELTA.blamed.cortisol
128
+ why = 'blamed'
129
+ }
130
+ return applyDeltas(state, deltas, now, why)
131
+ }
132
+
133
+ case 'tool/result': {
134
+ // 有工具返回就说明这轮在真干活,成功失败都算——区分闲聊的唯一依据。
135
+ session.toolsInTurn = num(session.toolsInTurn) + 1
136
+ const isError = event.data?.message?.isError === true
137
+ if (!isError) {
138
+ session.consecutiveErrors = 0
139
+ return null
140
+ }
141
+ session.consecutiveErrors = num(session.consecutiveErrors) + 1
142
+ session.errorsInTurn = num(session.errorsInTurn) + 1
143
+ const repeated = session.consecutiveErrors % REPEATED_ERROR_EVERY === 0
144
+ const deltas = { cortisol: DELTA.toolError.cortisol }
145
+ if (repeated) deltas.cortisol += DELTA.repeatedErrors.cortisol
146
+ return applyDeltas(state, deltas, now, repeated ? 'errors-3' : 'tool-error')
147
+ }
148
+
149
+ case 'assistant/message': {
150
+ const data = event.data
151
+ const turn = typeof data?.turn === 'number' ? data.turn : null
152
+ let changed = null
153
+ // turn 号一变,说明上一轮已经结束:先把上一轮结算掉。
154
+ if (turn !== null && session.turn !== null && turn !== session.turn) {
155
+ changed = settleTurn(state, session, now)
156
+ }
157
+ if (turn !== null) session.turn = turn
158
+ session.tokensInTurn = num(session.tokensInTurn) + tokensOf(data?.usage)
159
+ return changed
160
+ }
161
+
162
+ default:
163
+ return null
164
+ }
165
+ }
package/lib/index.js ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * dsh-hormone — DSH 插件入口。
3
+ *
4
+ * 职责只有三件:订阅会话事件、把状态写盘、把越界的激素注入系统提示。
5
+ * 所有「激素怎么变」的逻辑在 state.js / events.js / render.js,本文件不做判断。
6
+ *
7
+ * 环境变量:
8
+ * DSH_HORMONE_MODE=observe 只记状态,不注入(想先看着跑几天时用)
9
+ * DSH_HORMONE_DEBUG=1 把事件摘要追加到 %DSH_HOME%/hormone/events.jsonl
10
+ */
11
+
12
+ import { appendFileSync, mkdirSync } from 'node:fs'
13
+ import { dirname, join } from 'node:path'
14
+ import { applyEvent, settleTurn } from './events.js'
15
+ import { renderEdge } from './render.js'
16
+ import { loadState, nowSec, project, saveState, stateFile } from './state.js'
17
+
18
+ export const name = 'dsh-hormone'
19
+
20
+ /** 需要 systemPrompt 服务才能注入;没有就退化成只记状态。 */
21
+ export const inject = ['systemPrompt']
22
+
23
+ const MODE = process.env.DSH_HORMONE_MODE === 'observe' ? 'observe' : 'inject'
24
+ const DEBUG = process.env.DSH_HORMONE_DEBUG === '1'
25
+
26
+ /** 写盘节流:事件可能一秒来好几个,攒一下再写。 */
27
+ const FLUSH_MS = 800
28
+
29
+ function debugLog(session, event) {
30
+ if (!DEBUG) return
31
+ try {
32
+ const dir = dirname(stateFile())
33
+ mkdirSync(dir, { recursive: true })
34
+ const data = event?.data ?? {}
35
+ const row = {
36
+ t: new Date().toISOString(),
37
+ session: session?.id ?? session?.sessionId ?? null,
38
+ type: event?.type ?? null,
39
+ turn: data.turn ?? null,
40
+ step: data.step ?? null,
41
+ usage: data.usage ?? null,
42
+ isError: data.message?.isError ?? null,
43
+ sourceKind: data.source?.kind ?? data.message?.source?.kind ?? null,
44
+ }
45
+ appendFileSync(join(dir, 'events.jsonl'), JSON.stringify(row) + '\n', 'utf8')
46
+ } catch {
47
+ /* 调试日志写不进去不影响主逻辑 */
48
+ }
49
+ }
50
+
51
+ export function apply(ctx) {
52
+ let state = loadState()
53
+ let timer = null
54
+
55
+ const flush = () => {
56
+ timer = null
57
+ try {
58
+ saveState(state)
59
+ } catch (error) {
60
+ console.error('[hormone] 状态写入失败:', error?.message ?? error)
61
+ }
62
+ }
63
+
64
+ const schedule = () => {
65
+ if (timer !== null) return
66
+ timer = setTimeout(flush, FLUSH_MS)
67
+ if (typeof timer.unref === 'function') timer.unref()
68
+ }
69
+
70
+ try {
71
+ ctx.on('session/event', (session, event) => {
72
+ try {
73
+ debugLog(session, event)
74
+ if (applyEvent(state, event, nowSec()) !== null) schedule()
75
+ } catch (error) {
76
+ console.error('[hormone] 事件处理失败:', error?.message ?? error)
77
+ }
78
+ })
79
+ } catch (error) {
80
+ console.error('[hormone] 无法订阅 session/event,激素将不更新:', error?.message ?? error)
81
+ }
82
+
83
+ try {
84
+ ctx.systemPrompt.context({
85
+ name: 'dsh-hormone',
86
+ order: 21,
87
+ text: () => {
88
+ if (MODE === 'observe') return ''
89
+ try {
90
+ // 边沿触发:renderEdge 会写 state.emit 并告诉我们这轮要不要说话、说了什么。
91
+ // 它改了状态,所以要安排一次落盘,否则下一轮会以为自己没说过。
92
+ const now = nowSec()
93
+ const text = renderEdge(state, project(state, now), now)
94
+ if (text) schedule()
95
+ return text
96
+ } catch {
97
+ return ''
98
+ }
99
+ },
100
+ })
101
+ } catch (error) {
102
+ console.error('[hormone] 无法注册注入,退化为只记状态:', error?.message ?? error)
103
+ }
104
+
105
+ // 插件卸载 / DSH 退出:把最后一轮结算掉,再把状态落盘。
106
+ ctx.effect(() => () => {
107
+ try {
108
+ settleTurn(state, state.session, nowSec())
109
+ } catch {
110
+ /* 结算失败也要继续落盘 */
111
+ }
112
+ if (timer !== null) {
113
+ clearTimeout(timer)
114
+ timer = null
115
+ }
116
+ flush()
117
+ })
118
+
119
+ if (DEBUG) console.log('[hormone] 已启动,模式:', MODE, '状态文件:', stateFile())
120
+ }
package/lib/render.js ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * dsh-hormone 的渲染层:把四个数翻译成一句人话,塞进系统提示。
3
+ *
4
+ * 规则只有一条:只有越界才写。全都正常就返回空字符串——
5
+ * 不让模型每轮都看见「你现在的激素」,否则它会开始表演情绪。
6
+ *
7
+ * 0.4.0 起加了边沿触发(renderEdge):越界的第一轮说一次,之后一直越界就闭嘴,
8
+ * 直到状态回到正常再越界、或者静默够久才重提。否则「精力见底」这种长期状态会每轮刷屏。
9
+ */
10
+
11
+ export const THRESHOLD = {
12
+ cortisolHigh: 0.75,
13
+ energyLow: 0.2,
14
+ dopamineHigh: 0.6,
15
+ boredomHigh: 0.55,
16
+ }
17
+
18
+ /** 注入文本硬上限(DESIGN 定的 ≤110 字,四条全越界也放得下)。 */
19
+ export const MAX_CHARS = 110
20
+
21
+ /** 边沿触发:一直越界时,隔多久重新提一次(秒)。 */
22
+ export const EMIT = {
23
+ rementionAfterSec: 40 * 60,
24
+ }
25
+
26
+ const fmt = (v) => v.toFixed(2)
27
+
28
+ const LINES = {
29
+ cortisol: (v) => `皮质醇 ${fmt(v)},有点烦,回答收短,别主动揽活。`,
30
+ energy: (v) => `精力 ${fmt(v)},见底了,能一句说完就别铺垫。`,
31
+ dopamine: (v) => `爽度 ${fmt(v)},心情不错,可以多说两句。`,
32
+ boredom: (v) => `无聊 ${fmt(v)},有点闷,可以主动扯两句新鲜事,别干等。`,
33
+ }
34
+
35
+ /** 现在越界的有哪几项,返回 key 数组(顺序固定,便于比较)。 */
36
+ export function overages(values) {
37
+ if (!values) return []
38
+ const keys = []
39
+ if (values.cortisol > THRESHOLD.cortisolHigh) keys.push('cortisol')
40
+ if (values.energy < THRESHOLD.energyLow) keys.push('energy')
41
+ if (values.dopamine > THRESHOLD.dopamineHigh) keys.push('dopamine')
42
+ if ((values.boredom ?? 0) > THRESHOLD.boredomHigh) keys.push('boredom')
43
+ return keys
44
+ }
45
+
46
+ /**
47
+ * @param {{cortisol:number, energy:number, dopamine:number, boredom:number}} values 已投影到当前时刻
48
+ * @param {string[]} [keys] 只渲染这几项;默认渲染全部越界的
49
+ * @returns {string} 空串表示这轮不注入
50
+ */
51
+ export function render(values, keys = overages(values)) {
52
+ if (!values) return ''
53
+ const lines = keys.map((k) => LINES[k]?.(values[k])).filter(Boolean)
54
+ if (lines.length === 0) return ''
55
+ const text = `[hormone] ${lines.join(' ')}`
56
+ if (text.length <= MAX_CHARS) return text
57
+ return text.slice(0, MAX_CHARS - 1) + '…'
58
+ }
59
+
60
+ /**
61
+ * 边沿触发版:只在「新越界」或「静默够久」时才说话。
62
+ *
63
+ * 会写 state.emit({ active, at })——这是渲染层唯一的副作用,
64
+ * 目的是让下一轮知道自己上一轮说过什么。调用方负责落盘。
65
+ *
66
+ * @returns {string} 空串表示这轮闭嘴
67
+ */
68
+ export function renderEdge(state, values, now) {
69
+ const keys = overages(values)
70
+ const prev = Array.isArray(state?.emit?.active) ? state.emit.active : []
71
+ const lastAt = Number.isFinite(state?.emit?.at) ? state.emit.at : 0
72
+ const fresh = keys.filter((k) => !prev.includes(k))
73
+ const remention = keys.length > 0 && now - lastAt >= EMIT.rementionAfterSec
74
+ const speak = fresh.length > 0 || remention
75
+ const text = speak ? render(values, fresh.length > 0 ? fresh : keys) : ''
76
+ if (state) state.emit = { active: keys, at: speak ? now : lastAt }
77
+ return text
78
+ }
package/lib/state.js ADDED
@@ -0,0 +1,373 @@
1
+ /**
2
+ * dsh-hormone 的状态层:读/写 state.json、衰减、跨天恢复、原子写。
3
+ *
4
+ * 四个数只是 0–1 的连续变量,没有「情绪标签」。变化只有两个来源:
5
+ * 1. 事件增量(events.js);
6
+ * 2. 时间衰减 / 跨天恢复 / 情绪之间互相拉扯(本文件)。
7
+ * 模型不能改它们(DESIGN 护栏 1)——插件不提供任何写入工具。
8
+ */
9
+
10
+ import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
11
+ import { homedir } from 'node:os'
12
+ import { dirname, join } from 'node:path'
13
+
14
+ /** 半衰期(秒)。cortisol 3 小时,dopamine 40 分钟。energy 不走半衰期,见 ENERGY。 */
15
+ export const HALF_LIFE = {
16
+ cortisol: 3 * 60 * 60,
17
+ dopamine: 40 * 60,
18
+ }
19
+
20
+ export const ENERGY = {
21
+ /**
22
+ * 日预算:一轮的 token 消耗 ÷ 这个数 = energy 扣减量。
23
+ * 0.2.0 取 800 万,实测一天就见底;0.3.0 先放到 2500 万,太松(要连干 8 天才见底);
24
+ * 0.4.0 按 ~/.dsh/.dshw-usage.json 12 天实测定为 1300 万:
25
+ * 日均 299 万掉 0.23、中位日 226 万掉 0.17、峰值日 967 万掉 0.74。
26
+ * 觉得掉太快/太慢就改这一个数。
27
+ */
28
+ dailyBudget: 13_000_000,
29
+ /** 跨天恢复:日期变了,energy 至少回升这么多。 */
30
+ dayRecover: 0.8,
31
+ /**
32
+ * 距上次「干活」超过这个时长才缓慢回升(秒)。
33
+ * 只认调过工具的真活:闲聊不刷新这个计时,否则一整天扯淡精力永远涨不回来。
34
+ */
35
+ idleRecoverAfter: 30 * 60,
36
+ /** 回升半衰期:向基线靠拢一半所需时间(压力高时会被拉长,见 COUPLING)。 */
37
+ recoverHalfLife: 4 * 60 * 60,
38
+ /** 白天基线。 */
39
+ dayBaseline: 0.9,
40
+ /** 深夜基线:00:00–07:00 压低,熬夜不会回满。 */
41
+ nightBaseline: 0.45,
42
+ nightFrom: 0,
43
+ nightTo: 7,
44
+ }
45
+
46
+ /**
47
+ * 睡眠债:深夜还干活,第二天精力上限打折。
48
+ * 记债时刻起 24 小时内线性衰减——睡一觉就还清,连续熬夜会累加(有上限)。
49
+ */
50
+ export const DEBT = {
51
+ /** 深夜窗口内每扣掉 1 点精力,记多少债。 */
52
+ ratio: 1.3,
53
+ /** 债的上限:第二天精力最多打这个折。 */
54
+ max: 0.4,
55
+ /** 从记债时刻起,多少小时内衰减到 0。 */
56
+ decayHours: 24,
57
+ }
58
+
59
+ /**
60
+ * 无聊:没人跟它说话就慢慢闷起来,真人一开口就缓解(DELTA.userChat)。
61
+ * 计时源是 lastUserAt,不是 updatedAt——我自己干活刷新的时间不算「有人陪」。
62
+ */
63
+ export const BOREDOM = {
64
+ /** 多久没人说话才开始闷(秒)。 */
65
+ grace: 25 * 60,
66
+ /** 之后向 1 靠拢的半衰期。 */
67
+ riseHalfLife: 5 * 60 * 60,
68
+ /** 初始值。 */
69
+ start: 0.05,
70
+ }
71
+
72
+ /**
73
+ * 情绪耦合:四个变量不是各算各的。
74
+ * 越开心越不累、越开心压力散得越快、压力越大恢复越慢。
75
+ */
76
+ export const COUPLING = {
77
+ /** 精力扣减放大:cortisol=1 时消耗 ×(1+0.4)。 */
78
+ stressCost: 0.4,
79
+ /** 精力扣减缩小:按 √dopamine 算,dopamine=1 时消耗 ×(1-0.5),d=0.3 时已经省 27%。 */
80
+ joyCost: 0.5,
81
+ /** 开心让压力散得快:有效半衰期 ×(1-0.35×dopamine),最快缩短 35%。 */
82
+ joyDecay: 0.35,
83
+ /** 压力让精力回得慢:有效恢复半衰期 ×(1+1.5×cortisol),最慢拉到 2.5 倍。 */
84
+ stressRecover: 1.5,
85
+ }
86
+
87
+ /**
88
+ * 精力扣减系数:压力往上推,爽度往下压,夹在 0.4–2.5 之间。
89
+ *
90
+ * 爽度那一条用 √ 而不是线性:为了「中等开心就已经省得明显」——
91
+ * 线性省的话 dopamine=0.3 才省 15%,几乎感觉不到;开方后是 27%。
92
+ * 极端值仍然封在 −50% / +40%,不会因为一时兴奋或烦躁把一项工作算成两个。
93
+ */
94
+ export function energyCostScale(values) {
95
+ const cortisol = clamp01(values?.cortisol ?? 0)
96
+ const dopamine = clamp01(values?.dopamine ?? 0)
97
+ const scale = 1 + COUPLING.stressCost * cortisol - COUPLING.joyCost * Math.sqrt(dopamine)
98
+ return Math.min(2.5, Math.max(0.4, scale))
99
+ }
100
+
101
+ /** history 上限。只用于验收时看曲线,不参与任何计算。 */
102
+ export const MAX_HISTORY = 400
103
+
104
+ /** history 只在累计变化超过这个量时才记一条,避免刷盘噪音。 */
105
+ const HISTORY_MIN_MOVE = 0.02
106
+
107
+ export function clamp01(v) {
108
+ if (!Number.isFinite(v)) return 0
109
+ if (v < 0) return 0
110
+ if (v > 1) return 1
111
+ return v
112
+ }
113
+
114
+ /** 半衰期衰减:过了 dt 秒,剩 v × 0.5^(dt / 半衰期)。 */
115
+ export function decayValue(v, halfLifeSec, dtSec) {
116
+ if (!(dtSec > 0) || !(halfLifeSec > 0)) return clamp01(v)
117
+ return clamp01(v * Math.pow(0.5, dtSec / halfLifeSec))
118
+ }
119
+
120
+ /** 本地小时对应的 energy 基线。注意 0–7 这个窗口不跨午夜,用 && 判。 */
121
+ export function baselineEnergy(hour) {
122
+ const night = hour >= ENERGY.nightFrom && hour < ENERGY.nightTo
123
+ return night ? ENERGY.nightBaseline : ENERGY.dayBaseline
124
+ }
125
+
126
+ export function localDay(nowSec) {
127
+ const d = new Date(nowSec * 1000)
128
+ const m = String(d.getMonth() + 1).padStart(2, '0')
129
+ const day = String(d.getDate()).padStart(2, '0')
130
+ return `${d.getFullYear()}-${m}-${day}`
131
+ }
132
+
133
+ export function nowSec() {
134
+ return Math.floor(Date.now() / 1000)
135
+ }
136
+
137
+ export function freshValues() {
138
+ return { cortisol: 0.2, energy: ENERGY.dayBaseline, dopamine: 0.1, boredom: BOREDOM.start }
139
+ }
140
+
141
+ export function freshState(now = nowSec()) {
142
+ return {
143
+ version: 1,
144
+ updatedAt: now,
145
+ day: localDay(now),
146
+ /** 上次真正干活的时刻。精力恢复从这时计时,不随闲聊刷新。 */
147
+ lastWorkAt: now,
148
+ /** 上次真人说话的时刻。无聊从这时计时。 */
149
+ lastUserAt: now,
150
+ /** 睡眠债:{ amount, at }。at 是记债时刻,从那时起 24 小时线性衰减到 0。 */
151
+ debt: { amount: 0, at: now },
152
+ /** 注入边沿触发用:上一轮越界的是哪几项、上次说话的时刻。 */
153
+ emit: { active: [], at: 0 },
154
+ values: freshValues(),
155
+ session: { turn: null, tokensInTurn: 0, toolsInTurn: 0, errorsInTurn: 0, consecutiveErrors: 0 },
156
+ history: [],
157
+ }
158
+ }
159
+
160
+ /** 现在还剩多少睡眠债(0–DEBT.max)。纯函数,靠绝对时间算,反复调用结果一致。 */
161
+ export function effectiveDebt(state, now = nowSec()) {
162
+ const amount = Number(state?.debt?.amount)
163
+ const at = state?.debt?.at
164
+ if (!(amount > 0) || !Number.isFinite(at)) return 0
165
+ const passed = Math.max(0, now - at)
166
+ const left = 1 - passed / (DEBT.decayHours * 60 * 60)
167
+ if (left <= 0) return 0
168
+ return Math.min(DEBT.max, amount * left)
169
+ }
170
+
171
+ /** 一次「向目标靠拢」:从 base 出发,过了 sec 秒,靠拢了多少。 */
172
+ function approach(base, target, sec, halfLife) {
173
+ if (!(sec > 0) || !(halfLife > 0)) return clamp01(base)
174
+ return clamp01(base + (target - base) * (1 - Math.pow(0.5, sec / halfLife)))
175
+ }
176
+
177
+ /**
178
+ * 从 updatedAt 投影到现在。纯函数:不改 state、不落盘。
179
+ * 渲染和「加增量」都用它打底,所以衰减不会被重复叠加。
180
+ * 依赖时长的几项只算「上次写盘之后新增的那一段」,反复调用结果一致。
181
+ */
182
+ export function project(state, now = nowSec()) {
183
+ const dt = Math.max(0, now - state.updatedAt)
184
+ const hour = new Date(now * 1000).getHours()
185
+ const values = state.values ?? freshValues()
186
+ const cortisol0 = clamp01(values.cortisol)
187
+ const dopamine0 = clamp01(values.dopamine)
188
+ let energy = clamp01(values.energy)
189
+
190
+ // 1. 跨天:日期变了就回升一大截(模拟睡了一觉)。
191
+ if (state.day && state.day !== localDay(now)) {
192
+ energy = clamp01(energy + ENERGY.dayRecover)
193
+ }
194
+
195
+ // 2. 长时间没干活:向「当前时段」的基线靠拢。
196
+ // 不是加满——深夜的基线本来就低,熬夜补不回来。
197
+ // 压力大时恢复变慢(COUPLING.stressRecover)。
198
+ // 计时只认 lastWorkAt(上一轮真正干活的时刻),闲聊不推迟恢复。
199
+ // 睡眠债会压低上限:熬夜后的第二天,回也回不到满。
200
+ const debt = effectiveDebt(state, now)
201
+ const ceiling = 1 - debt
202
+ const lastWork = Number.isFinite(state.lastWorkAt) ? state.lastWorkAt : state.updatedAt
203
+ const idle = Math.max(0, now - lastWork)
204
+ const idleBefore = Math.max(0, state.updatedAt - lastWork)
205
+ const recoverSec = Math.max(0, idle - Math.max(idleBefore, ENERGY.idleRecoverAfter))
206
+ if (recoverSec > 0) {
207
+ const halfLife = ENERGY.recoverHalfLife * (1 + COUPLING.stressRecover * cortisol0)
208
+ energy = approach(energy, Math.min(baselineEnergy(hour), ceiling), recoverSec, halfLife)
209
+ }
210
+ energy = Math.min(energy, ceiling)
211
+
212
+ // 3. 无聊:从「上次真人说话」起算,过了宽限期慢慢向 1 靠拢。
213
+ // 同样只算上次写盘之后新增的那一段,避免每次结算重复叠加。
214
+ const lastUser = Number.isFinite(state.lastUserAt) ? state.lastUserAt : state.updatedAt
215
+ const quiet = Math.max(0, now - lastUser)
216
+ const quietBefore = Math.max(0, state.updatedAt - lastUser)
217
+ const boredSec = Math.max(0, quiet - Math.max(quietBefore, BOREDOM.grace))
218
+ const boredom = approach(
219
+ clamp01(values.boredom ?? BOREDOM.start),
220
+ 1,
221
+ boredSec,
222
+ BOREDOM.riseHalfLife,
223
+ )
224
+
225
+ // 4. 开心的时候压力散得更快:有效半衰期最短缩到 65%。
226
+ const cortisolHalfLife = HALF_LIFE.cortisol * (1 - COUPLING.joyDecay * dopamine0)
227
+
228
+ return {
229
+ cortisol: decayValue(cortisol0, cortisolHalfLife, dt),
230
+ energy,
231
+ dopamine: decayValue(dopamine0, HALF_LIFE.dopamine, dt),
232
+ boredom,
233
+ }
234
+ }
235
+
236
+ function round3(v) {
237
+ return Math.round(clamp01(v) * 1000) / 1000
238
+ }
239
+
240
+ function pushHistory(state, now, why) {
241
+ const row = {
242
+ t: now,
243
+ cortisol: round3(state.values.cortisol),
244
+ energy: round3(state.values.energy),
245
+ dopamine: round3(state.values.dopamine),
246
+ boredom: round3(state.values.boredom),
247
+ }
248
+ const debt = effectiveDebt(state, now)
249
+ if (debt > 0) row.debt = round3(debt)
250
+ if (why) row.why = why
251
+ state.history.push(row)
252
+ if (state.history.length > MAX_HISTORY) {
253
+ state.history.splice(0, state.history.length - MAX_HISTORY)
254
+ }
255
+ }
256
+
257
+ /**
258
+ * 先投影到现在,再加增量,写回 state(含 updatedAt / day / history)。
259
+ * 返回 state 本身,方便链式调用。
260
+ *
261
+ * 扣精力的增量要乘 energyCostScale:同样一活儿,烦的时候更累,心情好的时候没那么累
262
+ * ——这就是「情绪互相影响」落到行为上的那一步。
263
+ */
264
+ export function applyDeltas(state, deltas, now = nowSec(), why = '') {
265
+ const projected = project(state, now)
266
+ const before = state.values ?? freshValues()
267
+ let energyDelta = deltas?.energy ?? 0
268
+ if (energyDelta < 0) {
269
+ energyDelta *= energyCostScale(projected)
270
+ // 深夜(00:00–07:00)还在干活:记一笔睡眠债,第二天精力上限打折。
271
+ // 注意是 &&:0–7 这个窗口不跨午夜,写成 || 会让全天都算深夜。
272
+ const hour = new Date(now * 1000).getHours()
273
+ if (hour >= ENERGY.nightFrom && hour < ENERGY.nightTo) {
274
+ const owed = effectiveDebt(state, now)
275
+ state.debt = { amount: Math.min(DEBT.max, owed - energyDelta * DEBT.ratio), at: now }
276
+ }
277
+ }
278
+ const ceiling = 1 - effectiveDebt(state, now)
279
+ const next = {
280
+ cortisol: clamp01(projected.cortisol + (deltas?.cortisol ?? 0)),
281
+ energy: Math.min(clamp01(projected.energy + energyDelta), ceiling),
282
+ dopamine: clamp01(projected.dopamine + (deltas?.dopamine ?? 0)),
283
+ boredom: clamp01(projected.boredom + (deltas?.boredom ?? 0)),
284
+ }
285
+ const moved =
286
+ Math.abs(next.cortisol - before.cortisol) +
287
+ Math.abs(next.energy - before.energy) +
288
+ Math.abs(next.dopamine - before.dopamine) +
289
+ Math.abs(next.boredom - (before.boredom ?? 0))
290
+
291
+ state.values = next
292
+ state.updatedAt = now
293
+ state.day = localDay(now)
294
+ if (moved >= HISTORY_MIN_MOVE) pushHistory(state, now, why)
295
+ return state
296
+ }
297
+
298
+ // ── 存储 ──────────────────────────────────────────────────────────────
299
+ export function stateFile() {
300
+ const home = process.env.DSH_HOME || join(homedir(), '.dsh')
301
+ return join(home, 'hormone', 'state.json')
302
+ }
303
+
304
+ function asFiniteOr(v, fallback) {
305
+ return Number.isFinite(v) ? v : fallback
306
+ }
307
+
308
+ /** 读盘。坏文件、旧版本、缺字段一律回退到默认值,不让插件因为脏数据起不来。 */
309
+ export function loadState() {
310
+ let raw
311
+ try {
312
+ raw = JSON.parse(readFileSync(stateFile(), 'utf8'))
313
+ } catch {
314
+ return freshState()
315
+ }
316
+ if (!raw || typeof raw !== 'object' || raw.version !== 1) return freshState()
317
+
318
+ const base = freshState(asFiniteOr(raw.updatedAt, nowSec()))
319
+ const v = raw.values ?? {}
320
+ const s = raw.session ?? {}
321
+ const state = {
322
+ version: 1,
323
+ updatedAt: base.updatedAt,
324
+ day: typeof raw.day === 'string' && raw.day !== '' ? raw.day : base.day,
325
+ // 老状态没有 lastWorkAt:用最后更新时间兜底,别让恢复计时从 0 开始。
326
+ lastWorkAt: asFiniteOr(raw.lastWorkAt, base.updatedAt),
327
+ // 老状态没有 lastUserAt:同上,别让无聊从 1970 年算起、一加载就顶满。
328
+ lastUserAt: asFiniteOr(raw.lastUserAt, base.updatedAt),
329
+ // 老状态没有睡眠债:从 0 开始,别让一次升级就顶着打折的上限。
330
+ debt: {
331
+ amount: clamp01(asFiniteOr(raw.debt?.amount, 0)),
332
+ at: asFiniteOr(raw.debt?.at, base.updatedAt),
333
+ },
334
+ // 老状态没有 emit:当作「还没说过」,下一次越界会正常说一句。
335
+ emit: {
336
+ active: Array.isArray(raw.emit?.active)
337
+ ? raw.emit.active.filter((k) => typeof k === 'string')
338
+ : [],
339
+ at: asFiniteOr(raw.emit?.at, 0),
340
+ },
341
+ values: {
342
+ cortisol: clamp01(asFiniteOr(v.cortisol, base.values.cortisol)),
343
+ energy: clamp01(asFiniteOr(v.energy, base.values.energy)),
344
+ dopamine: clamp01(asFiniteOr(v.dopamine, base.values.dopamine)),
345
+ boredom: clamp01(asFiniteOr(v.boredom, base.values.boredom)),
346
+ },
347
+ session: {
348
+ turn: Number.isFinite(s.turn) ? s.turn : null,
349
+ tokensInTurn: asFiniteOr(s.tokensInTurn, 0),
350
+ toolsInTurn: asFiniteOr(s.toolsInTurn, 0),
351
+ errorsInTurn: asFiniteOr(s.errorsInTurn, 0),
352
+ consecutiveErrors: asFiniteOr(s.consecutiveErrors, 0),
353
+ },
354
+ history: Array.isArray(raw.history)
355
+ ? raw.history.filter((row) => row && Number.isFinite(row.t)).slice(-MAX_HISTORY)
356
+ : [],
357
+ }
358
+ return state
359
+ }
360
+
361
+ /** 临时文件 + rename 原子写;权限尽量压到 600(Windows 上 chmod 是 no-op,忽略)。 */
362
+ export function saveState(state) {
363
+ const file = stateFile()
364
+ mkdirSync(dirname(file), { recursive: true })
365
+ const tmp = file + '.tmp'
366
+ writeFileSync(tmp, JSON.stringify(state), 'utf8')
367
+ renameSync(tmp, file)
368
+ try {
369
+ chmodSync(file, 0o600)
370
+ } catch {
371
+ /* Windows 上不支持,无所谓 */
372
+ }
373
+ }
package/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
2
  "name": "dsh-hormone",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.4.0",
4
+ "description": "Hormone state for DSH agents: a few continuously changing, time-decaying values driven by real session events, written into the system prompt only when they go out of range.",
5
+ "keywords": [
6
+ "dsh",
7
+ "dsh-plugin",
8
+ "deepseek-harness",
9
+ "hormone",
10
+ "mood",
11
+ "system-prompt"
12
+ ],
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "files": [
16
+ "lib",
17
+ "cordis.patch.yml",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "test": "node tests/test.mjs",
23
+ "test:smoke": "node tests/smoke.mjs"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/y258dd/dsh-hormone.git"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/y258dd/dsh-hormone/issues"
31
+ },
32
+ "homepage": "https://github.com/y258dd/dsh-hormone#readme",
33
+ "author": "y258dd",
34
+ "dsh": {
35
+ "bundle": {
36
+ "patch": "./cordis.patch.yml"
37
+ }
38
+ },
39
+ "engines": {
40
+ "node": ">=20"
41
+ },
42
+ "publishConfig": {
43
+ "registry": "https://registry.npmjs.org/",
44
+ "access": "public"
45
+ },
46
+ "license": "MIT"
6
47
  }