Skip to content

Commit 3ea4e6f

Browse files
committed
feat(message): enhance message state management with createMessage method for runtime adaptability
1 parent 471dbcb commit 3ea4e6f

File tree

6 files changed

+116
-18
lines changed

6 files changed

+116
-18
lines changed

packages/kit/src/message/adapters/native.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ export const createNativeMessageAdapter = (): MessageStateAdapter => {
116116
return {
117117
initialize,
118118
getState,
119+
createMessage(message) {
120+
return message
121+
},
119122
mutate,
120123
subscribe,
121124
}

packages/kit/src/message/adapters/vue.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,14 @@ export const createVueMessageAdapter = (): VueMessageStateAdapter => {
5555

5656
requestState.value = initialState.requestState
5757
processingState.value = initialState.processingState
58-
messages.value.push(...initialState.messages)
58+
messages.value = initialState.messages.map(toReactiveMessage)
5959
initialized = true
6060
}
6161

62+
const createMessage = <T extends ChatMessage>(message: T): T => {
63+
return toReactiveMessage(message) as T
64+
}
65+
6266
const getState = () => {
6367
if (!initialized) {
6468
throw new Error('Message state adapter is not initialized')
@@ -94,7 +98,7 @@ export const createVueMessageAdapter = (): VueMessageStateAdapter => {
9498
return messages.value
9599
},
96100
set messages(value) {
97-
messages.value = value.map(toReactiveMessage)
101+
messages.value = value.map(createMessage)
98102
},
99103
}
100104

@@ -177,6 +181,7 @@ export const createVueMessageAdapter = (): VueMessageStateAdapter => {
177181
isProcessing,
178182
initialize,
179183
getState,
184+
createMessage,
180185
mutate,
181186
subscribe,
182187
}

packages/kit/src/message/core/engine.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export const createMessageEngine = (
9494
const plugins = deduplicatePlugins(defaultPlugins.concat(pluginsFromOptions))
9595

9696
const getState = () => adapter.getState()
97+
const createMessage = <T extends ChatMessage>(message: T): T => adapter.createMessage(message)
9798
const subscribe = adapter.subscribe
9899
const mutate = adapter.mutate
99100

@@ -137,16 +138,21 @@ export const createMessageEngine = (
137138
}
138139

139140
const appendMessages = (...messages: ChatMessage[]) => {
141+
const runtimeMessages = messages.map((message) => createMessage(message))
142+
140143
mutate('messages', (draft) => {
141-
draft.messages.push(...messages)
144+
draft.messages.push(...runtimeMessages)
142145
})
143146

144-
runtime.currentTurn.push(...messages)
147+
runtime.currentTurn.push(...runtimeMessages)
148+
149+
return runtimeMessages
145150
}
146151

147152
// Create base context for plugins
148153
const getBaseContext = (abortSignal: AbortSignal): BasePluginContext => ({
149154
getState,
155+
createMessage,
150156
mutate,
151157
abortSignal,
152158
currentTurn: runtime.currentTurn,
@@ -174,9 +180,9 @@ export const createMessageEngine = (
174180
// 请求前对消息进行清洗,去掉不必要的字段
175181
requestBody.messages = sanitizeMessages(requestBody.messages)
176182

177-
const assistantMessage = { role: 'assistant', content: '', loading: true } as ChatMessage
183+
let assistantMessage = { role: 'assistant', content: '', loading: true } as ChatMessage
184+
;[assistantMessage] = appendMessages(assistantMessage)
178185
options.setAssistantMessage?.(assistantMessage)
179-
appendMessages(assistantMessage)
180186

181187
const result = responseProvider(requestBody, abortSignal)
182188
const chunks = normalizeToAsyncGenerator(result)
@@ -194,7 +200,7 @@ export const createMessageEngine = (
194200
}
195201
})
196202

197-
const choice = chunk.choices.find((item) => item.index === 0) ?? chunk.choices[0]
203+
const choice = (chunk.choices || []).find((item) => item.index === 0) ?? chunk.choices?.[0]
198204

199205
if (!choice) {
200206
continue

packages/kit/src/message/plugins/toolPlugin.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ type ToolCallContext = BasePluginContext & {
2323
function fillMissingToolMessages({
2424
messages,
2525
cancelledContent,
26+
createMessage,
2627
mutate,
2728
}: {
2829
messages: ChatMessage[]
2930
cancelledContent: string
31+
createMessage: BasePluginContext['createMessage']
3032
mutate: MutateMessageStateFn
3133
}): void {
3234
// 第一阶段:从首位开始遍历,收集需要插入的信息
@@ -80,11 +82,13 @@ function fillMissingToolMessages({
8082
mutate('messages', (draft) => {
8183
for (let i = insertInfos.length - 1; i >= 0; i--) {
8284
const { insertAfterIndex, missingToolCallIds } = insertInfos[i]
83-
const cancelledMessages: ChatMessage[] = missingToolCallIds.map((toolCallId) => ({
84-
role: 'tool',
85-
tool_call_id: toolCallId,
86-
content: cancelledContent,
87-
}))
85+
const cancelledMessages: ChatMessage[] = missingToolCallIds.map((toolCallId) =>
86+
createMessage({
87+
role: 'tool',
88+
tool_call_id: toolCallId,
89+
content: cancelledContent,
90+
}),
91+
)
8892

8993
// 在 assistant 消息之后插入所有取消消息
9094
draft.messages.splice(insertAfterIndex + 1, 0, ...cancelledMessages)
@@ -197,11 +201,11 @@ export const toolPlugin = (
197201
name: 'tool',
198202
...restOptions,
199203
onTurnStart: (context) => {
200-
const { getState, mutate } = context
204+
const { getState, createMessage, mutate } = context
201205
const messages = getState().messages
202206

203207
if (autoFillMissingToolMessages) {
204-
fillMissingToolMessages({ messages, cancelledContent: toolCallCancelledContent, mutate })
208+
fillMissingToolMessages({ messages, cancelledContent: toolCallCancelledContent, createMessage, mutate })
205209
}
206210

207211
return restOptions.onTurnStart?.(context)
@@ -217,7 +221,16 @@ export const toolPlugin = (
217221
return restOptions.onBeforeRequest?.(context)
218222
},
219223
onAfterRequest: async (context) => {
220-
const { currentMessage, lastChoice, appendMessage, abortSignal, setRequestState, requestNext, mutate } = context
224+
const {
225+
currentMessage,
226+
lastChoice,
227+
appendMessage,
228+
abortSignal,
229+
setRequestState,
230+
requestNext,
231+
mutate,
232+
createMessage,
233+
} = context
221234

222235
if (lastChoice?.finish_reason !== 'tool_calls' || !currentMessage.tool_calls?.length) {
223236
return
@@ -231,15 +244,15 @@ export const toolPlugin = (
231244

232245
const toolCallPromises = currentMessage.tool_calls.map(async (toolCall) => {
233246
const now = Math.floor(Date.now() / 1000)
234-
const toolMessage: ChatMessage = {
247+
const toolMessage: ChatMessage = createMessage({
235248
role: 'tool',
236249
tool_call_id: toolCall.id,
237250
content: '',
238251
metadata: {
239252
createdAt: now,
240253
updatedAt: now,
241254
},
242-
}
255+
})
243256

244257
appendMessage(toolMessage)
245258

packages/kit/src/message/test/vue.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,41 @@ describe('createVueMessageAdapter', () => {
8787
expect(snapshot).toMatchObject(expectedMessageSnapshots[idx])
8888
})
8989
})
90+
91+
it('updates nested message content through the reactive assistant message reference', async () => {
92+
const adapter = createVueMessageAdapter()
93+
const engine = createMessageEngine(adapter, {
94+
plugins: silentDefaultPlugins,
95+
responseProvider: mockResponseProvider(['hello', ' world']),
96+
})
97+
98+
const contentSnapshots: string[] = []
99+
let stopWatch = () => {}
100+
101+
watch(
102+
adapter.messages,
103+
(messages) => {
104+
const assistantMessage = messages.find((message) => message.role === 'assistant')
105+
stopWatch()
106+
if (!assistantMessage) {
107+
return
108+
}
109+
110+
stopWatch = watch(
111+
() => assistantMessage.content,
112+
(content) => {
113+
contentSnapshots.push(content as string)
114+
},
115+
{ flush: 'sync', immediate: true },
116+
)
117+
},
118+
{ flush: 'sync', immediate: true },
119+
)
120+
121+
await engine.sendMessage('ping')
122+
stopWatch()
123+
124+
const distinctSnapshots = contentSnapshots.filter((content, index) => content !== contentSnapshots[index - 1])
125+
expect(distinctSnapshots).toEqual(['', 'hello', 'hello world'])
126+
})
90127
})

packages/kit/src/message/types.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,32 @@ export type MessageUpdateKinds = MessageUpdateKind | MessageUpdateKind[]
7575
export type MessageMutationRecipe = (draft: InternalMessageState, skipNotify: () => void) => void
7676

7777
export interface MutateMessageStateFn {
78+
/**
79+
* 在受控上下文中修改消息状态,并按声明的更新类型分发状态变更通知。
80+
*
81+
* 这里的“通知”语义对应 `subscribe` 订阅链路,用于驱动依赖 engine 状态快照的观察者;
82+
* 它并不等同于具体框架的响应式更新机制。对于 Vue 等运行时,界面更新可能同时依赖
83+
* 框架自身的响应式追踪与 `subscribe` 侧的状态通知,两者职责不同,不应混用。
84+
*/
7885
(kinds: MessageUpdateKinds, recipe: MessageMutationRecipe): void
7986
}
8087

8188
export interface MessageStateAdapter {
8289
initialize(initialState: InternalMessageState): void
8390
getState(): PublicMessageState
91+
/**
92+
* 创建一条适配当前运行时的消息对象。
93+
*
94+
* engine 和 core 插件里凡是“新建消息”的入口,都应统一走这个方法,
95+
* 以便不同平台注入各自的运行时能力。
96+
*
97+
* 例如:
98+
* - native/纯 TS 场景:直接返回原对象即可
99+
* `createMessage(message) { return message }`
100+
* - Vue 场景:返回 reactive proxy,使后续对 message.content / message.state 的原地修改能够被追踪
101+
* `createMessage(message) { return reactive(message) }`
102+
*/
103+
createMessage<T extends ChatMessage>(message: T): T
84104
mutate: MutateMessageStateFn
85105
subscribe(listener: (state: PublicMessageState) => void): () => void
86106
subscribe(kinds: MessageUpdateKinds, listener: (state: PublicMessageState) => void): () => void
@@ -89,7 +109,21 @@ export interface MessageStateAdapter {
89109
export interface BasePluginContext {
90110
getState(): PublicMessageState
91111
/**
92-
* 使用 mutate 函数修改消息,才能正常触发消息更新通知。
112+
* 创建一条适配当前运行时的消息对象。
113+
*
114+
* 插件在运行过程中如果需要主动创建并追加消息,应统一通过此接口构造消息实例,
115+
* 而不是直接持有普通对象字面量。这样可以确保消息对象具备当前平台所要求的运行时能力。
116+
*
117+
* 对于采用响应式机制的平台(例如 Vue),该接口通常会返回带有响应式包装的消息实例。
118+
* 如果插件绕过此接口直接创建普通对象,再参与后续的状态更新或原地字段修改,
119+
* 可能导致消息内容、状态字段等变更无法被正确追踪。
120+
*/
121+
createMessage<T extends ChatMessage>(message: T): T
122+
/**
123+
* 在受控上下文中修改状态,并触发与 `subscribe` 对应的状态更新通知。
124+
*
125+
* 该通知机制面向 engine 的订阅接口,而不是具体框架的响应式系统本身。
126+
* 因此,在需要让 `subscribe` 观察者感知到变更时,应通过此接口完成状态写入。
93127
*/
94128
mutate: MutateMessageStateFn
95129
abortSignal: AbortSignal

0 commit comments

Comments
 (0)