@linxin666/dsh-pet 0.3.19 → 0.3.20
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 +1 -1
- package/README.zh.md +1 -1
- package/lib/client.js +70 -33
- package/lib/client.js.map +1 -1
- package/lib/index.js +75 -57
- package/lib/types/client/renderers/frames2d.d.ts.map +1 -1
- package/lib/types/client/renderers/frames2d.js +77 -29
- package/lib/types/event-projection.d.ts +14 -0
- package/lib/types/event-projection.d.ts.map +1 -1
- package/lib/types/event-projection.js +32 -18
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +11 -1
- package/package.json +14 -14
- package/src/client/renderers/frames2d.test.ts +94 -4
- package/src/client/renderers/frames2d.ts +76 -26
- package/src/event-projection.ts +37 -18
- package/src/service.ts +11 -0
|
@@ -213,33 +213,73 @@ export const frames2dRenderer: PetRenderer<PetFrames2dConfig> = {
|
|
|
213
213
|
// the last painted frame instead of breaking playback).
|
|
214
214
|
const decoding = new Map<string, Promise<DecodedFrame | undefined>>()
|
|
215
215
|
const decodedAll: Promise<void>[] = []
|
|
216
|
-
|
|
216
|
+
// All frame fetches funnel through a small pool. Full-library warm passes
|
|
217
|
+
// on large pets fire 1100+ requests at once, which trips the browser's
|
|
218
|
+
// in-flight request limit (net::ERR_INSUFFICIENT_RESOURCES) and fails
|
|
219
|
+
// whole batches of frames while starving the rest of the page. Playback
|
|
220
|
+
// demand jumps ahead of the warm backlog.
|
|
221
|
+
const FRAME_POOL_LIMIT = 8
|
|
222
|
+
const frameQueue: Array<{ url: string; release: () => void }> = []
|
|
223
|
+
let activeFrames = 0
|
|
224
|
+
|
|
225
|
+
const decodeFrame = async (url: string): Promise<DecodedFrame | undefined> => {
|
|
226
|
+
try {
|
|
227
|
+
const response = await fetch(url)
|
|
228
|
+
if (!response.ok) throw new Error('http ' + response.status)
|
|
229
|
+
const bitmap = await createImageBitmap(await response.blob())
|
|
230
|
+
return { source: bitmap, width: bitmap.width, height: bitmap.height }
|
|
231
|
+
} catch {
|
|
232
|
+
// Fail-open: classic Image decode keeps non-modern runtimes alive.
|
|
233
|
+
return await new Promise<DecodedFrame | undefined>((resolve) => {
|
|
234
|
+
try {
|
|
235
|
+
const pre = new Image()
|
|
236
|
+
pre.onload = (): void => {
|
|
237
|
+
resolve(pre.naturalWidth > 0 ? { source: pre, width: pre.naturalWidth, height: pre.naturalHeight } : undefined)
|
|
238
|
+
}
|
|
239
|
+
pre.onerror = (): void => resolve(undefined)
|
|
240
|
+
pre.src = url
|
|
241
|
+
} catch {
|
|
242
|
+
resolve(undefined)
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const pumpFrames = (): void => {
|
|
249
|
+
while (activeFrames < FRAME_POOL_LIMIT && frameQueue.length > 0) {
|
|
250
|
+
const queued = frameQueue.shift()!
|
|
251
|
+
activeFrames += 1
|
|
252
|
+
queued.release()
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const loadFrame = (url: string, jump = false): Promise<DecodedFrame | undefined> => {
|
|
257
|
+
// Jump first: warm-enqueued frames already carry their memo, so a
|
|
258
|
+
// playback demand must reorder the unstarted entry before the cache
|
|
259
|
+
// lookup short-circuits.
|
|
260
|
+
if (jump) {
|
|
261
|
+
const index = frameQueue.findIndex((queued) => queued.url === url)
|
|
262
|
+
if (index > 0) frameQueue.unshift(frameQueue.splice(index, 1)[0]!)
|
|
263
|
+
}
|
|
217
264
|
const cached = decoding.get(url)
|
|
218
265
|
if (cached !== undefined) return cached
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
pre.onload = (): void => {
|
|
231
|
-
resolve(pre.naturalWidth > 0 ? { source: pre, width: pre.naturalWidth, height: pre.naturalHeight } : undefined)
|
|
232
|
-
}
|
|
233
|
-
pre.onerror = (): void => resolve(undefined)
|
|
234
|
-
pre.src = url
|
|
235
|
-
} catch {
|
|
236
|
-
resolve(undefined)
|
|
237
|
-
}
|
|
238
|
-
})
|
|
239
|
-
}
|
|
240
|
-
})()
|
|
266
|
+
let release!: () => void
|
|
267
|
+
const gate = new Promise<void>((resolve) => { release = resolve })
|
|
268
|
+
const job: Promise<DecodedFrame | undefined> = gate.then(() => (disposed ? undefined : decodeFrame(url)))
|
|
269
|
+
job.then(
|
|
270
|
+
(frame) => { if (frame === undefined) decoding.delete(url) },
|
|
271
|
+
() => decoding.delete(url),
|
|
272
|
+
)
|
|
273
|
+
void job.finally(() => {
|
|
274
|
+
activeFrames -= 1
|
|
275
|
+
pumpFrames()
|
|
276
|
+
})
|
|
241
277
|
decoding.set(url, job)
|
|
242
278
|
decodedAll.push(job.then(() => undefined, () => undefined))
|
|
279
|
+
const entry = { url, release }
|
|
280
|
+
if (jump) frameQueue.unshift(entry)
|
|
281
|
+
else frameQueue.push(entry)
|
|
282
|
+
pumpFrames()
|
|
243
283
|
return job
|
|
244
284
|
}
|
|
245
285
|
|
|
@@ -268,7 +308,7 @@ export const frames2dRenderer: PetRenderer<PetFrames2dConfig> = {
|
|
|
268
308
|
const paintCanvas = (url: string): void => {
|
|
269
309
|
if (context2d === null || canvas === null) return
|
|
270
310
|
const myToken = ++drawToken
|
|
271
|
-
void loadFrame(url).then((frame) => {
|
|
311
|
+
void loadFrame(url, true).then((frame) => {
|
|
272
312
|
if (disposed || frame === undefined || myToken !== drawToken) return
|
|
273
313
|
if (lastDrawnUrl === url) return
|
|
274
314
|
lastDrawnUrl = url
|
|
@@ -363,7 +403,15 @@ export const frames2dRenderer: PetRenderer<PetFrames2dConfig> = {
|
|
|
363
403
|
// Warm pass: decode every frame up front (tiny same-origin webp files)
|
|
364
404
|
// so loops and phase switches never wait on a first decode - same intent
|
|
365
405
|
// as the historical Image-cache warm loop, now feeding the decode cache.
|
|
366
|
-
|
|
406
|
+
// Phase-reachable tracks enqueue first so early switches never trail the
|
|
407
|
+
// full warm backlog; demand loads jump the queue regardless.
|
|
408
|
+
const warmTrackIds: string[] = [
|
|
409
|
+
...new Set([...Object.values(config.phases), config.phases.idle]),
|
|
410
|
+
]
|
|
411
|
+
for (const warmTrack of [...warmTrackIds, ...Object.keys(config.tracks)].map(
|
|
412
|
+
(id) => config.tracks[id],
|
|
413
|
+
)) {
|
|
414
|
+
if (warmTrack === undefined) continue
|
|
367
415
|
for (const warmUrl of warmTrack.frames) void loadFrame(warmUrl)
|
|
368
416
|
}
|
|
369
417
|
|
|
@@ -378,7 +426,9 @@ export const frames2dRenderer: PetRenderer<PetFrames2dConfig> = {
|
|
|
378
426
|
if (timer !== undefined) clearTimeout(timer)
|
|
379
427
|
if (watchdog !== undefined) clearInterval(watchdog)
|
|
380
428
|
// Release decoded bitmaps after pending decodes settle; close() is
|
|
381
|
-
// browser-only, so guard it for exotic hosts.
|
|
429
|
+
// browser-only, so guard it for exotic hosts. Queued-but-unstarted
|
|
430
|
+
// frames release immediately as no-ops so the settle barrier drains.
|
|
431
|
+
for (const queued of frameQueue.splice(0)) queued.release()
|
|
382
432
|
void Promise.allSettled(decodedAll).then(() => {
|
|
383
433
|
for (const job of decoding.values()) {
|
|
384
434
|
void job.then((frame) => {
|
package/src/event-projection.ts
CHANGED
|
@@ -11,9 +11,16 @@
|
|
|
11
11
|
* tool/result and turn/end — so the pet's inner voice always roughly knows
|
|
12
12
|
* what is going on and never mis-fires on output text. The wall clock is
|
|
13
13
|
* injected by the caller, keeping every projection reproducible.
|
|
14
|
+
*
|
|
15
|
+
* Since the 0.1.5-alpha.2 cohort the stream itself is no longer durable
|
|
16
|
+
* vocabulary: per-chunk phase input arrives through the process-local
|
|
17
|
+
* `agent/assistant-stream` publication ({@link projectAssistantStreamFrame}),
|
|
18
|
+
* while the durable log settles one `assistant/message` (or `assistant/attempt`)
|
|
19
|
+
* per attempt.
|
|
14
20
|
* @module @linxin666/dsh-pet/event-projection
|
|
15
21
|
*/
|
|
16
22
|
|
|
23
|
+
import type { AssistantStreamFrame } from '@deepseek-ai/dsh-agent'
|
|
17
24
|
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
|
18
25
|
import type { PetStateInput } from './state.ts'
|
|
19
26
|
import {
|
|
@@ -102,24 +109,6 @@ export function projectOfficialEvent(
|
|
|
102
109
|
runtime.activeTools.clear()
|
|
103
110
|
runtime.stepHadFailure = false
|
|
104
111
|
return { input: { phase: 'waiting', line: runtime.voice.scene('waiting', nowMs) } }
|
|
105
|
-
case 'assistant/chunk': {
|
|
106
|
-
const { chunk } = event.data
|
|
107
|
-
if (chunk.type === 'reasoning-delta' && chunk.text.length > 0) {
|
|
108
|
-
const whisper = runtime.whispers.feed('thinking', nowMs)
|
|
109
|
-
return {
|
|
110
|
-
input: { phase: 'thinking', line: runtime.voice.scene('thinking', nowMs) },
|
|
111
|
-
...(whisper === undefined ? {} : { whisper }),
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
if (chunk.type === 'text-delta' && chunk.text.length > 0) {
|
|
115
|
-
const whisper = runtime.whispers.feed('writing', nowMs)
|
|
116
|
-
return {
|
|
117
|
-
input: { phase: 'review', line: runtime.voice.scene('review', nowMs) },
|
|
118
|
-
...(whisper === undefined ? {} : { whisper }),
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return undefined
|
|
122
|
-
}
|
|
123
112
|
case 'assistant/message':
|
|
124
113
|
return { input: { phase: 'review', line: runtime.voice.scene('review', nowMs) } }
|
|
125
114
|
case 'tool/call': {
|
|
@@ -207,3 +196,33 @@ export function projectOfficialEvent(
|
|
|
207
196
|
return undefined
|
|
208
197
|
}
|
|
209
198
|
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Project one live `agent/assistant-stream` publication into the pet's visual
|
|
202
|
+
* phases. Chunk frames are the alpha.2 replacement for the retired durable
|
|
203
|
+
* `assistant/chunk` event: a reasoning delta keeps the pet thinking, a text
|
|
204
|
+
* delta moves it to review; start, end, and non-delta chunks change nothing.
|
|
205
|
+
*/
|
|
206
|
+
export function projectAssistantStreamFrame(
|
|
207
|
+
frame: AssistantStreamFrame,
|
|
208
|
+
runtime: ProjectionRuntime,
|
|
209
|
+
nowMs: number = Date.now(),
|
|
210
|
+
): PetActivityTransition | undefined {
|
|
211
|
+
if (frame.type !== 'chunk') return undefined
|
|
212
|
+
const { chunk } = frame
|
|
213
|
+
if (chunk.type === 'reasoning-delta' && chunk.text.length > 0) {
|
|
214
|
+
const whisper = runtime.whispers.feed('thinking', nowMs)
|
|
215
|
+
return {
|
|
216
|
+
input: { phase: 'thinking', line: runtime.voice.scene('thinking', nowMs) },
|
|
217
|
+
...(whisper === undefined ? {} : { whisper }),
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (chunk.type === 'text-delta' && chunk.text.length > 0) {
|
|
221
|
+
const whisper = runtime.whispers.feed('writing', nowMs)
|
|
222
|
+
return {
|
|
223
|
+
input: { phase: 'review', line: runtime.voice.scene('review', nowMs) },
|
|
224
|
+
...(whisper === undefined ? {} : { whisper }),
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return undefined
|
|
228
|
+
}
|
package/src/service.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { Context, Service } from '@deepseek-ai/cordis'
|
|
16
|
+
import type { AssistantStreamFrame } from '@deepseek-ai/dsh-agent'
|
|
16
17
|
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
|
|
17
18
|
import type { AffinityConfig, PetAffinityView, PetInteraction } from './affinity.ts'
|
|
18
19
|
import { announcementFresh, parseAnnouncement, type PetAnnouncement } from './announce.ts'
|
|
@@ -20,6 +21,7 @@ import type { TreatConfig } from './treats.ts'
|
|
|
20
21
|
import {
|
|
21
22
|
emptyProjectionRuntime,
|
|
22
23
|
isActivityPhase,
|
|
24
|
+
projectAssistantStreamFrame,
|
|
23
25
|
projectOfficialEvent,
|
|
24
26
|
type ActivityStatusEventLike,
|
|
25
27
|
type ProjectionRuntime,
|
|
@@ -466,6 +468,15 @@ export class PetService extends Service {
|
|
|
466
468
|
this.rewardTurn(String(session.id), transition.completedTurn)
|
|
467
469
|
}
|
|
468
470
|
}),
|
|
471
|
+
this.ctx.on('agent/assistant-stream', ({ agent, frame }: { agent: { session: Session }; frame: AssistantStreamFrame }) => {
|
|
472
|
+
const session = agent.session
|
|
473
|
+
const runtime = this.activityOf(session).runtime
|
|
474
|
+
const transition = projectAssistantStreamFrame(frame, runtime)
|
|
475
|
+
if (transition === undefined) return
|
|
476
|
+
runtime.officialEventsSeen = true
|
|
477
|
+
this.officialEventSessions.add(session)
|
|
478
|
+
this.applyActivity(session, transition.input, transition.whisper)
|
|
479
|
+
}),
|
|
469
480
|
this.ctx.on('session/disposed', (session: Session) => {
|
|
470
481
|
this.ledger.forgetSession(String(session.id))
|
|
471
482
|
this.officialEventSessions.delete(session)
|