@linxin666/dsh-pet 0.1.13 → 0.1.14
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/README.i18n.yaml +2 -2
- package/README.md +11 -7
- package/README.zh.md +11 -7
- package/lib/client.js +9 -1
- package/lib/client.js.map +1 -1
- package/lib/index.js +164 -25
- package/lib/invariant.js +1 -1
- package/lib/{state-C9EyycwI.js → state-P-wsVJ6O.js} +7 -6
- package/lib/types/client/WhalePet.d.ts.map +1 -1
- package/lib/types/client/WhalePet.js +2 -1
- package/lib/types/service.d.ts +16 -5
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +159 -35
- package/lib/types/state.d.ts +10 -12
- package/lib/types/state.d.ts.map +1 -1
- package/lib/types/state.js +14 -10
- package/package.json +1 -1
- package/src/client/WhalePet.test.tsx +25 -1
- package/src/client/WhalePet.tsx +6 -0
- package/src/client/pet.module.css +9 -0
- package/src/service.ts +187 -34
- package/src/state.test.ts +4 -1
- package/src/state.ts +17 -13
package/lib/types/state.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pet state machine — pure, clock-injected. Maps
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* transitions the web UI exposes (turn end celebration, no-session idle).
|
|
2
|
+
* Pet state machine — pure, clock-injected. Maps official DSH session activity
|
|
3
|
+
* and the legacy `activity/status` vocabulary onto the 9-state Codex pet
|
|
4
|
+
* animation contract, plus turn-end celebration and no-session idle.
|
|
6
5
|
*
|
|
7
6
|
* The machine is deliberately dumb: it holds the last input phase, the
|
|
8
7
|
* animation decision, and a one-shot "celebration" window after `done` so the
|
|
@@ -10,13 +9,13 @@
|
|
|
10
9
|
* function of (input, nowMs); persistence and RPC live in the service.
|
|
11
10
|
* @module @linxin666/dsh-pet/state
|
|
12
11
|
*/
|
|
13
|
-
/**
|
|
14
|
-
export type ActivityPhase = 'idle' | 'waiting' | 'thinking' | 'tool' | 'done';
|
|
12
|
+
/** Activity phases understood by the pet host. */
|
|
13
|
+
export type ActivityPhase = 'idle' | 'waiting' | 'thinking' | 'tool' | 'review' | 'done' | 'failed';
|
|
15
14
|
/** The Codex-compatible 9-state animation contract (spritesheet rows). */
|
|
16
15
|
export type PetAnimation = 'idle' | 'running-right' | 'running-left' | 'waving' | 'jumping' | 'failed' | 'waiting' | 'running' | 'review';
|
|
17
16
|
/** One input snapshot consumed by the machine. */
|
|
18
17
|
export interface PetStateInput {
|
|
19
|
-
/** Current
|
|
18
|
+
/** Current activity phase of the active session. */
|
|
20
19
|
phase: ActivityPhase;
|
|
21
20
|
/** Human-readable status line (plain text). */
|
|
22
21
|
line?: string;
|
|
@@ -44,13 +43,12 @@ export interface PetStateConfig {
|
|
|
44
43
|
export declare const defaultPetStateConfig: PetStateConfig;
|
|
45
44
|
/**
|
|
46
45
|
* Map one activity phase onto the animation contract.
|
|
47
|
-
* - thinking
|
|
48
|
-
*
|
|
46
|
+
* - thinking → `running` and tool → `running-right` (focused work).
|
|
47
|
+
* - review → `review` while answer text is streaming.
|
|
49
48
|
* - waiting → `waiting` (expectant pose, needs user input).
|
|
50
49
|
* - done → `jumping` (celebration), then back to `idle` after the window.
|
|
50
|
+
* - failed → `failed` until another activity event arrives.
|
|
51
51
|
* - idle → `idle` (calm breathing loop).
|
|
52
|
-
* `failed` has no DSH phase source yet; the machine keeps the mapping table
|
|
53
|
-
* so a future error event can light it up.
|
|
54
52
|
*/
|
|
55
53
|
export declare function animationForPhase(phase: ActivityPhase): PetAnimation;
|
|
56
54
|
/** The spritesheet row index for one animation track. */
|
|
@@ -68,7 +66,7 @@ export declare class PetStateMachine {
|
|
|
68
66
|
private sessionActive;
|
|
69
67
|
private doneAt;
|
|
70
68
|
constructor(config?: PetStateConfig, now?: () => number);
|
|
71
|
-
/** Consume one
|
|
69
|
+
/** Consume one projected activity update. */
|
|
72
70
|
onActivityStatus(input: PetStateInput): void;
|
|
73
71
|
/** A session became the active one (or a fresh session started). */
|
|
74
72
|
onSessionActive(): void;
|
package/lib/types/state.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/state.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,kDAAkD;AAClD,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAA;AAEnG,0EAA0E;AAC1E,MAAM,MAAM,YAAY,GACpB,MAAM,GACN,eAAe,GACf,cAAc,GACd,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,SAAS,GACT,SAAS,GACT,QAAQ,CAAA;AAEZ,kDAAkD;AAClD,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,KAAK,EAAE,aAAa,CAAA;IACpB,+CAA+C;IAC/C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,4DAA4D;AAC5D,MAAM,WAAW,gBAAgB;IAC/B,qCAAqC;IACrC,SAAS,EAAE,YAAY,CAAA;IACvB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,oEAAoE;IACpE,kBAAkB,EAAE,MAAM,CAAA;IAC1B,oEAAoE;IACpE,KAAK,EAAE,aAAa,CAAA;IACpB,0DAA0D;IAC1D,aAAa,EAAE,OAAO,CAAA;CACvB;AAED,6BAA6B;AAC7B,MAAM,WAAW,cAAc;IAC7B,kFAAkF;IAClF,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,eAAO,MAAM,qBAAqB,EAAE,cAAsC,CAAA;AAE1E;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,GAAG,YAAY,CAUpE;AAED,yDAAyD;AACzD,wBAAgB,KAAK,CAAC,SAAS,EAAE,YAAY,GAAG,MAAM,CAarD;AAED;;;GAGG;AACH,qBAAa,eAAe;IAQxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;IARtB,OAAO,CAAC,KAAK,CAAwB;IACrC,OAAO,CAAC,IAAI,CAAoB;IAChC,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,MAAM,CAAoB;gBAGf,MAAM,GAAE,cAAsC,EAC9C,GAAG,GAAE,MAAM,MAAiB;IAG/C,6CAA6C;IAC7C,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAO5C,oEAAoE;IACpE,eAAe,IAAI,IAAI;IAIvB,sDAAsD;IACtD,iBAAiB,IAAI,IAAI;IAQzB,6CAA6C;IAC7C,MAAM,IAAI,gBAAgB;CAwB3B"}
|
package/lib/types/state.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pet state machine — pure, clock-injected. Maps
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* transitions the web UI exposes (turn end celebration, no-session idle).
|
|
2
|
+
* Pet state machine — pure, clock-injected. Maps official DSH session activity
|
|
3
|
+
* and the legacy `activity/status` vocabulary onto the 9-state Codex pet
|
|
4
|
+
* animation contract, plus turn-end celebration and no-session idle.
|
|
6
5
|
*
|
|
7
6
|
* The machine is deliberately dumb: it holds the last input phase, the
|
|
8
7
|
* animation decision, and a one-shot "celebration" window after `done` so the
|
|
@@ -13,20 +12,21 @@
|
|
|
13
12
|
export const defaultPetStateConfig = { celebrateMs: 2400 };
|
|
14
13
|
/**
|
|
15
14
|
* Map one activity phase onto the animation contract.
|
|
16
|
-
* - thinking
|
|
17
|
-
*
|
|
15
|
+
* - thinking → `running` and tool → `running-right` (focused work).
|
|
16
|
+
* - review → `review` while answer text is streaming.
|
|
18
17
|
* - waiting → `waiting` (expectant pose, needs user input).
|
|
19
18
|
* - done → `jumping` (celebration), then back to `idle` after the window.
|
|
19
|
+
* - failed → `failed` until another activity event arrives.
|
|
20
20
|
* - idle → `idle` (calm breathing loop).
|
|
21
|
-
* `failed` has no DSH phase source yet; the machine keeps the mapping table
|
|
22
|
-
* so a future error event can light it up.
|
|
23
21
|
*/
|
|
24
22
|
export function animationForPhase(phase) {
|
|
25
23
|
switch (phase) {
|
|
26
24
|
case 'thinking': return 'running';
|
|
27
25
|
case 'tool': return 'running-right';
|
|
26
|
+
case 'review': return 'review';
|
|
28
27
|
case 'waiting': return 'waiting';
|
|
29
28
|
case 'done': return 'jumping';
|
|
29
|
+
case 'failed': return 'failed';
|
|
30
30
|
case 'idle': return 'idle';
|
|
31
31
|
}
|
|
32
32
|
}
|
|
@@ -61,7 +61,7 @@ export class PetStateMachine {
|
|
|
61
61
|
this.config = config;
|
|
62
62
|
this.now = now;
|
|
63
63
|
}
|
|
64
|
-
/** Consume one
|
|
64
|
+
/** Consume one projected activity update. */
|
|
65
65
|
onActivityStatus(input) {
|
|
66
66
|
this.phase = input.phase;
|
|
67
67
|
this.line = input.line;
|
|
@@ -94,7 +94,11 @@ export class PetStateMachine {
|
|
|
94
94
|
animation = 'idle';
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
const bubble = this.
|
|
97
|
+
const bubble = this.phase === 'done'
|
|
98
|
+
&& this.doneAt !== undefined
|
|
99
|
+
&& nowMs - this.doneAt >= this.config.celebrateMs
|
|
100
|
+
? undefined
|
|
101
|
+
: this.phrase ?? this.line;
|
|
98
102
|
return {
|
|
99
103
|
animation,
|
|
100
104
|
...(bubble === undefined ? {} : { bubble }),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-pet",
|
|
3
3
|
"description": "鲸鱼娘宠物插件 for the dsh web GUI: a soft healing whale-girl companion that reacts to model activity (idle/waiting/thinking/tool/done), with petting/feeding interactions and an affinity score",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.14",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -55,7 +55,7 @@ afterEach(() => {
|
|
|
55
55
|
})
|
|
56
56
|
|
|
57
57
|
/** Render the pet with mocked callbacks; returns the rename spy. */
|
|
58
|
-
function renderPet(): { onRename: ReturnType<typeof vi.fn> } {
|
|
58
|
+
function renderPet(overrides: Partial<WhalePetProps> = {}): { onRename: ReturnType<typeof vi.fn> } {
|
|
59
59
|
const onRename = vi.fn()
|
|
60
60
|
const props: WhalePetProps = {
|
|
61
61
|
snapshot,
|
|
@@ -68,6 +68,7 @@ function renderPet(): { onRename: ReturnType<typeof vi.fn> } {
|
|
|
68
68
|
onRename,
|
|
69
69
|
onFeedbackDone: vi.fn(),
|
|
70
70
|
t,
|
|
71
|
+
...overrides,
|
|
71
72
|
}
|
|
72
73
|
render(<WhalePet {...props} />)
|
|
73
74
|
return { onRename }
|
|
@@ -136,3 +137,26 @@ describe('WhalePet rename input', () => {
|
|
|
136
137
|
expect(screen.queryByPlaceholderText('输入新名字')).toBeNull()
|
|
137
138
|
})
|
|
138
139
|
})
|
|
140
|
+
|
|
141
|
+
describe('WhalePet status bubble', () => {
|
|
142
|
+
const workingSnapshot: PetStateView = {
|
|
143
|
+
...snapshot,
|
|
144
|
+
animation: 'running',
|
|
145
|
+
phase: 'thinking',
|
|
146
|
+
bubble: '正在思考',
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
it('renders host activity when no interaction feedback is active', () => {
|
|
150
|
+
renderPet({ snapshot: workingSnapshot })
|
|
151
|
+
expect(screen.queryByText('正在思考')).not.toBeNull()
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('lets transient interaction feedback replace host activity', () => {
|
|
155
|
+
renderPet({
|
|
156
|
+
snapshot: workingSnapshot,
|
|
157
|
+
feedback: { text: '摸摸成功', kind: 'pet', at: 1 },
|
|
158
|
+
})
|
|
159
|
+
expect(screen.queryByText('摸摸成功')).not.toBeNull()
|
|
160
|
+
expect(screen.queryByText('正在思考')).toBeNull()
|
|
161
|
+
})
|
|
162
|
+
})
|
package/src/client/WhalePet.tsx
CHANGED
|
@@ -228,6 +228,7 @@ export function WhalePet(props: WhalePetProps): ReactPortal {
|
|
|
228
228
|
const pos = dragPos ?? { right: display.right, bottom: display.bottom }
|
|
229
229
|
const spriteWidth = Math.round(FRAME_WIDTH * spriteScale)
|
|
230
230
|
const spriteHeight = Math.round(FRAME_HEIGHT * spriteScale)
|
|
231
|
+
const statusBubble = feedback === null && !hovered ? snapshot?.bubble : undefined
|
|
231
232
|
|
|
232
233
|
const float = (
|
|
233
234
|
<div
|
|
@@ -281,6 +282,11 @@ export function WhalePet(props: WhalePetProps): ReactPortal {
|
|
|
281
282
|
{feedback.text}
|
|
282
283
|
</div>
|
|
283
284
|
)}
|
|
285
|
+
{statusBubble !== undefined && (
|
|
286
|
+
<div className={`${styles.bubble} ${styles.bubbleStatus}`} role="status" aria-live="polite">
|
|
287
|
+
{statusBubble}
|
|
288
|
+
</div>
|
|
289
|
+
)}
|
|
284
290
|
{hovered && dragRef.current === null && (
|
|
285
291
|
<div
|
|
286
292
|
className={styles.panel}
|
|
@@ -37,6 +37,15 @@
|
|
|
37
37
|
background: rgba(56, 189, 248, 0.92);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
.bubbleStatus {
|
|
41
|
+
max-width: min(280px, calc(100vw - 24px));
|
|
42
|
+
overflow: hidden;
|
|
43
|
+
text-overflow: ellipsis;
|
|
44
|
+
background: rgba(15, 23, 42, 0.9);
|
|
45
|
+
border: 1px solid rgba(125, 211, 252, 0.5);
|
|
46
|
+
animation: none;
|
|
47
|
+
}
|
|
48
|
+
|
|
40
49
|
@keyframes pet-bubble-pop {
|
|
41
50
|
0% {
|
|
42
51
|
opacity: 0;
|
package/src/service.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pet host service — the `pet.*` RPC domain. Owns the state machine wiring
|
|
3
|
-
* (
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `pet.
|
|
7
|
-
* for browser consumers.
|
|
3
|
+
* (projects official session events and accepts legacy `activity/status`), the
|
|
4
|
+
* affinity ledger, and the persisted display config. The API gateway maps
|
|
5
|
+
* this service's methods onto `pet.state` / `pet.interact` /
|
|
6
|
+
* `pet.setVisible` / `pet.setConfig` for browser consumers.
|
|
8
7
|
* @module @linxin666/dsh-pet/service
|
|
9
8
|
*/
|
|
10
9
|
|
|
@@ -40,6 +39,7 @@ import {
|
|
|
40
39
|
defaultPetStateConfig,
|
|
41
40
|
PetStateMachine,
|
|
42
41
|
type PetStateConfig,
|
|
42
|
+
type PetStateInput,
|
|
43
43
|
type PetStateSnapshot,
|
|
44
44
|
} from './state.ts'
|
|
45
45
|
|
|
@@ -128,6 +128,119 @@ declare module '@deepseek-ai/cordis' {
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/** Runtime shape of the optional legacy activity event. */
|
|
132
|
+
interface ActivityStatusEventLike {
|
|
133
|
+
phase?: string
|
|
134
|
+
line?: string
|
|
135
|
+
phrase?: string
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Per-session facts needed to project the official event stream. */
|
|
139
|
+
interface SessionActivityRuntime {
|
|
140
|
+
activeTools: Set<string>
|
|
141
|
+
officialEventsSeen: boolean
|
|
142
|
+
stepHadFailure: boolean
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** One official event projection, optionally carrying a completed turn reward. */
|
|
146
|
+
interface PetActivityTransition {
|
|
147
|
+
input: PetStateInput
|
|
148
|
+
completedTurn?: number
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Keep tool names readable inside the compact status bubble. */
|
|
152
|
+
function displayToolName(name: string): string {
|
|
153
|
+
const compact = name.replace(/\s+/g, ' ').trim() || '工具'
|
|
154
|
+
return compact.length <= 24 ? compact : `${compact.slice(0, 21)}...`
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Whether a legacy phase is part of the pet's supported vocabulary. */
|
|
158
|
+
function isActivityPhase(phase: string): phase is PetStateInput['phase'] {
|
|
159
|
+
return ['idle', 'waiting', 'thinking', 'tool', 'review', 'done', 'failed'].includes(phase)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Project the durable DSH session vocabulary into the pet's visual phases.
|
|
164
|
+
* Unknown and log-only events do not disturb the last meaningful activity.
|
|
165
|
+
*/
|
|
166
|
+
function projectOfficialEvent(
|
|
167
|
+
event: SessionEvent,
|
|
168
|
+
runtime: SessionActivityRuntime,
|
|
169
|
+
): PetActivityTransition | undefined {
|
|
170
|
+
switch (event.type) {
|
|
171
|
+
case 'turn/start':
|
|
172
|
+
runtime.activeTools.clear()
|
|
173
|
+
runtime.stepHadFailure = false
|
|
174
|
+
return { input: { phase: 'waiting', line: '准备开始' } }
|
|
175
|
+
case 'step/start':
|
|
176
|
+
runtime.activeTools.clear()
|
|
177
|
+
runtime.stepHadFailure = false
|
|
178
|
+
return { input: { phase: 'waiting', line: '等待模型响应' } }
|
|
179
|
+
case 'assistant/chunk': {
|
|
180
|
+
const { chunk } = event.data
|
|
181
|
+
if (chunk.type === 'reasoning-delta' && chunk.text.length > 0) {
|
|
182
|
+
return { input: { phase: 'thinking', line: '正在思考' } }
|
|
183
|
+
}
|
|
184
|
+
if (chunk.type === 'text-delta' && chunk.text.length > 0) {
|
|
185
|
+
return { input: { phase: 'review', line: '整理回复中' } }
|
|
186
|
+
}
|
|
187
|
+
return undefined
|
|
188
|
+
}
|
|
189
|
+
case 'assistant/message':
|
|
190
|
+
return { input: { phase: 'review', line: '整理回复中' } }
|
|
191
|
+
case 'tool/call':
|
|
192
|
+
runtime.activeTools.add(String(event.data.callId))
|
|
193
|
+
return {
|
|
194
|
+
input: {
|
|
195
|
+
phase: 'tool',
|
|
196
|
+
line: `正在使用 ${displayToolName(event.data.name)}`,
|
|
197
|
+
},
|
|
198
|
+
}
|
|
199
|
+
case 'tool/result': {
|
|
200
|
+
const block = event.data.message.content[0]
|
|
201
|
+
runtime.activeTools.delete(String(event.data.message.source.callId))
|
|
202
|
+
runtime.stepHadFailure ||= event.data.error !== undefined || block.isError === true
|
|
203
|
+
if (runtime.activeTools.size > 0) {
|
|
204
|
+
return {
|
|
205
|
+
input: {
|
|
206
|
+
phase: 'tool',
|
|
207
|
+
line: `还有 ${runtime.activeTools.size} 个工具运行中`,
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return runtime.stepHadFailure
|
|
212
|
+
? { input: { phase: 'failed', line: '工具执行失败' } }
|
|
213
|
+
: { input: { phase: 'thinking', line: '处理工具结果' } }
|
|
214
|
+
}
|
|
215
|
+
case 'turn/end': {
|
|
216
|
+
runtime.activeTools.clear()
|
|
217
|
+
switch (event.data.reason.kind) {
|
|
218
|
+
case 'completed':
|
|
219
|
+
return {
|
|
220
|
+
input: { phase: 'done', line: '完成啦' },
|
|
221
|
+
completedTurn: event.data.turn,
|
|
222
|
+
}
|
|
223
|
+
case 'error':
|
|
224
|
+
return { input: { phase: 'failed', line: '执行失败' } }
|
|
225
|
+
case 'max-tokens':
|
|
226
|
+
return { input: { phase: 'failed', line: '达到输出上限' } }
|
|
227
|
+
case 'interrupted':
|
|
228
|
+
return { input: { phase: 'failed', line: '执行意外中断' } }
|
|
229
|
+
case 'blocked':
|
|
230
|
+
return { input: { phase: 'waiting', line: '等待继续' } }
|
|
231
|
+
case 'aborted':
|
|
232
|
+
return { input: { phase: 'idle', line: '已停止' } }
|
|
233
|
+
default:
|
|
234
|
+
// TurnEndReasonMap is merge-extensible; a newer ending must not
|
|
235
|
+
// leave the pet showing stale in-progress work.
|
|
236
|
+
return { input: { phase: 'idle' } }
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
default:
|
|
240
|
+
return undefined
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
131
244
|
/**
|
|
132
245
|
* Cordis service exposing the pet RPC domain. Lazy: nothing is scanned or
|
|
133
246
|
* written until a query or interaction arrives; event listeners update only
|
|
@@ -146,6 +259,10 @@ export class PetService extends Service {
|
|
|
146
259
|
private rewardedTurns = new Map<string, number>()
|
|
147
260
|
private enabled: boolean
|
|
148
261
|
private disposeActivity: (() => void) | undefined
|
|
262
|
+
/** Session whose most recent meaningful event currently drives the global pet. */
|
|
263
|
+
private displaySession: Session | undefined
|
|
264
|
+
private readonly sessionActivity = new WeakMap<Session, SessionActivityRuntime>()
|
|
265
|
+
private lastLegacyTurnRewardAt = 0
|
|
149
266
|
|
|
150
267
|
constructor(ctx: Context, config: PetConfig = {}) {
|
|
151
268
|
super(ctx, 'pet')
|
|
@@ -197,37 +314,38 @@ export class PetService extends Service {
|
|
|
197
314
|
this.disposeActivity = (() => {
|
|
198
315
|
const disposers = [
|
|
199
316
|
this.ctx.on('session/event', (session: Session, event: SessionEvent) => {
|
|
200
|
-
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
this.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
break
|
|
317
|
+
const runtime = this.activityRuntime(session)
|
|
318
|
+
// `activity/status` is an optional compatibility input. It is not
|
|
319
|
+
// declared as a durable event type by this package because current
|
|
320
|
+
// Harness installations publish the official session vocabulary.
|
|
321
|
+
if ((event.type as string) === 'activity/status') {
|
|
322
|
+
const payload = ((event as unknown as { data?: unknown }).data ?? {}) as ActivityStatusEventLike
|
|
323
|
+
if (typeof payload.phase !== 'string' || !isActivityPhase(payload.phase)) return
|
|
324
|
+
this.applyActivity(session, {
|
|
325
|
+
phase: payload.phase,
|
|
326
|
+
...(typeof payload.line === 'string' ? { line: payload.line } : {}),
|
|
327
|
+
...(typeof payload.phrase === 'string' ? { phrase: payload.phrase } : {}),
|
|
328
|
+
})
|
|
329
|
+
// On a legacy-only stream the compatibility event owns turn
|
|
330
|
+
// rewards. Once any official activity is observed, turn/end owns
|
|
331
|
+
// them and a derived legacy `done` cannot double-count.
|
|
332
|
+
if (payload.phase === 'done' && !runtime.officialEventsSeen) {
|
|
333
|
+
this.rewardLegacyTurn()
|
|
334
|
+
}
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const transition = projectOfficialEvent(event, runtime)
|
|
339
|
+
if (transition === undefined) return
|
|
340
|
+
runtime.officialEventsSeen = true
|
|
341
|
+
this.applyActivity(session, transition.input)
|
|
342
|
+
if (transition.completedTurn !== undefined) {
|
|
343
|
+
this.rewardTurn(String(session.id), transition.completedTurn)
|
|
228
344
|
}
|
|
229
345
|
}),
|
|
230
|
-
this.ctx.on('session/disposed', () => {
|
|
346
|
+
this.ctx.on('session/disposed', (session: Session) => {
|
|
347
|
+
if (session !== this.displaySession) return
|
|
348
|
+
this.displaySession = undefined
|
|
231
349
|
this.machine.onSessionDisposed()
|
|
232
350
|
}),
|
|
233
351
|
]
|
|
@@ -235,6 +353,27 @@ export class PetService extends Service {
|
|
|
235
353
|
})()
|
|
236
354
|
}
|
|
237
355
|
|
|
356
|
+
/** Return the projection state associated with one live session. */
|
|
357
|
+
private activityRuntime(session: Session): SessionActivityRuntime {
|
|
358
|
+
let runtime = this.sessionActivity.get(session)
|
|
359
|
+
if (runtime === undefined) {
|
|
360
|
+
runtime = {
|
|
361
|
+
activeTools: new Set(),
|
|
362
|
+
officialEventsSeen: false,
|
|
363
|
+
stepHadFailure: false,
|
|
364
|
+
}
|
|
365
|
+
this.sessionActivity.set(session, runtime)
|
|
366
|
+
}
|
|
367
|
+
return runtime
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Commit one activity as the host-global pet's most recent display state. */
|
|
371
|
+
private applyActivity(session: Session, input: PetStateInput): void {
|
|
372
|
+
this.displaySession = session
|
|
373
|
+
this.machine.onActivityStatus(input)
|
|
374
|
+
this.machine.onSessionActive()
|
|
375
|
+
}
|
|
376
|
+
|
|
238
377
|
/** RPC: pet or feed the pet. */
|
|
239
378
|
async interact(kind: PetInteraction): Promise<PetInteractResult> {
|
|
240
379
|
const nowMs = Date.now()
|
|
@@ -334,6 +473,20 @@ export class PetService extends Service {
|
|
|
334
473
|
const last = this.rewardedTurns.get(sessionId) ?? 0
|
|
335
474
|
if (turn <= last) return
|
|
336
475
|
this.rewardedTurns.set(sessionId, turn)
|
|
476
|
+
this.applyTurnReward()
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Preserve turn rewards for installations that only emit legacy activity. */
|
|
480
|
+
private rewardLegacyTurn(): void {
|
|
481
|
+
const nowMs = Date.now()
|
|
482
|
+
// A legacy `done` snapshot may repeat during the celebration window.
|
|
483
|
+
if (nowMs - this.lastLegacyTurnRewardAt < 5_000) return
|
|
484
|
+
this.lastLegacyTurnRewardAt = nowMs
|
|
485
|
+
this.applyTurnReward()
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Persist one accepted completed-turn reward. */
|
|
489
|
+
private applyTurnReward(): void {
|
|
337
490
|
this.persist = { ...this.persist, affinity: applyTurnReward(this.persist.affinity, this.affinityConfig) }
|
|
338
491
|
this.flush()
|
|
339
492
|
}
|
package/src/state.test.ts
CHANGED
|
@@ -11,8 +11,10 @@ describe('animationForPhase', () => {
|
|
|
11
11
|
it('maps each activity phase onto the animation contract', () => {
|
|
12
12
|
expect(animationForPhase('thinking')).toBe('running')
|
|
13
13
|
expect(animationForPhase('tool')).toBe('running-right')
|
|
14
|
+
expect(animationForPhase('review')).toBe('review')
|
|
14
15
|
expect(animationForPhase('waiting')).toBe('waiting')
|
|
15
16
|
expect(animationForPhase('done')).toBe('jumping')
|
|
17
|
+
expect(animationForPhase('failed')).toBe('failed')
|
|
16
18
|
expect(animationForPhase('idle')).toBe('idle')
|
|
17
19
|
})
|
|
18
20
|
})
|
|
@@ -27,7 +29,8 @@ describe('PetStateMachine', () => {
|
|
|
27
29
|
now += 2399
|
|
28
30
|
expect(machine.render().animation).toBe('jumping')
|
|
29
31
|
now += 2
|
|
30
|
-
expect(machine.render()
|
|
32
|
+
expect(machine.render()).toMatchObject({ animation: 'idle' })
|
|
33
|
+
expect(machine.render().bubble).toBeUndefined()
|
|
31
34
|
})
|
|
32
35
|
|
|
33
36
|
it('shows the phrase bubble when present, else the line', () => {
|
package/src/state.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pet state machine — pure, clock-injected. Maps
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* transitions the web UI exposes (turn end celebration, no-session idle).
|
|
2
|
+
* Pet state machine — pure, clock-injected. Maps official DSH session activity
|
|
3
|
+
* and the legacy `activity/status` vocabulary onto the 9-state Codex pet
|
|
4
|
+
* animation contract, plus turn-end celebration and no-session idle.
|
|
6
5
|
*
|
|
7
6
|
* The machine is deliberately dumb: it holds the last input phase, the
|
|
8
7
|
* animation decision, and a one-shot "celebration" window after `done` so the
|
|
@@ -11,8 +10,8 @@
|
|
|
11
10
|
* @module @linxin666/dsh-pet/state
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
|
-
/**
|
|
15
|
-
export type ActivityPhase = 'idle' | 'waiting' | 'thinking' | 'tool' | 'done'
|
|
13
|
+
/** Activity phases understood by the pet host. */
|
|
14
|
+
export type ActivityPhase = 'idle' | 'waiting' | 'thinking' | 'tool' | 'review' | 'done' | 'failed'
|
|
16
15
|
|
|
17
16
|
/** The Codex-compatible 9-state animation contract (spritesheet rows). */
|
|
18
17
|
export type PetAnimation =
|
|
@@ -28,7 +27,7 @@ export type PetAnimation =
|
|
|
28
27
|
|
|
29
28
|
/** One input snapshot consumed by the machine. */
|
|
30
29
|
export interface PetStateInput {
|
|
31
|
-
/** Current
|
|
30
|
+
/** Current activity phase of the active session. */
|
|
32
31
|
phase: ActivityPhase
|
|
33
32
|
/** Human-readable status line (plain text). */
|
|
34
33
|
line?: string
|
|
@@ -60,20 +59,21 @@ export const defaultPetStateConfig: PetStateConfig = { celebrateMs: 2400 }
|
|
|
60
59
|
|
|
61
60
|
/**
|
|
62
61
|
* Map one activity phase onto the animation contract.
|
|
63
|
-
* - thinking
|
|
64
|
-
*
|
|
62
|
+
* - thinking → `running` and tool → `running-right` (focused work).
|
|
63
|
+
* - review → `review` while answer text is streaming.
|
|
65
64
|
* - waiting → `waiting` (expectant pose, needs user input).
|
|
66
65
|
* - done → `jumping` (celebration), then back to `idle` after the window.
|
|
66
|
+
* - failed → `failed` until another activity event arrives.
|
|
67
67
|
* - idle → `idle` (calm breathing loop).
|
|
68
|
-
* `failed` has no DSH phase source yet; the machine keeps the mapping table
|
|
69
|
-
* so a future error event can light it up.
|
|
70
68
|
*/
|
|
71
69
|
export function animationForPhase(phase: ActivityPhase): PetAnimation {
|
|
72
70
|
switch (phase) {
|
|
73
71
|
case 'thinking': return 'running'
|
|
74
72
|
case 'tool': return 'running-right'
|
|
73
|
+
case 'review': return 'review'
|
|
75
74
|
case 'waiting': return 'waiting'
|
|
76
75
|
case 'done': return 'jumping'
|
|
76
|
+
case 'failed': return 'failed'
|
|
77
77
|
case 'idle': return 'idle'
|
|
78
78
|
}
|
|
79
79
|
}
|
|
@@ -110,7 +110,7 @@ export class PetStateMachine {
|
|
|
110
110
|
private readonly now: () => number = Date.now,
|
|
111
111
|
) {}
|
|
112
112
|
|
|
113
|
-
/** Consume one
|
|
113
|
+
/** Consume one projected activity update. */
|
|
114
114
|
onActivityStatus(input: PetStateInput): void {
|
|
115
115
|
this.phase = input.phase
|
|
116
116
|
this.line = input.line
|
|
@@ -144,7 +144,11 @@ export class PetStateMachine {
|
|
|
144
144
|
animation = 'idle'
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
-
const bubble = this.
|
|
147
|
+
const bubble = this.phase === 'done'
|
|
148
|
+
&& this.doneAt !== undefined
|
|
149
|
+
&& nowMs - this.doneAt >= this.config.celebrateMs
|
|
150
|
+
? undefined
|
|
151
|
+
: this.phrase ?? this.line
|
|
148
152
|
return {
|
|
149
153
|
animation,
|
|
150
154
|
...(bubble === undefined ? {} : { bubble }),
|