Skip to content

feat(message): extract core engine and migrate useMessage to adapters#325

Open
gene9831 wants to merge 16 commits intoopentiny:developfrom
gene9831:feat/kit-message-engine
Open

feat(message): extract core engine and migrate useMessage to adapters#325
gene9831 wants to merge 16 commits intoopentiny:developfrom
gene9831:feat/kit-message-engine

Conversation

@gene9831
Copy link
Copy Markdown
Collaborator

@gene9831 gene9831 commented Apr 15, 2026

背景

本次变更将 useMessage 的核心能力从 Vue 实现中抽离到 packages/kit/src/message/core,引入跨平台的 message engine + state adapter 模型,并完成 Vue useMessage 到 core 的迁移。

目标是把消息处理流程、插件生命周期和状态管理能力收敛到一套 core 实现中,避免 Vue 与非 Vue 场景分别维护两套逻辑。

架构图

flowchart TD
  A["Vue useMessage"] --> B["Core Engine"]
  B --> C["State Adapter"]
  B --> D["Core Plugins"]
  C --> E["Vue Reactive State"]
  C --> F["Native mutate/subscribe State"]

Loading

主要变更

  • 新增 message/core/engine.ts,统一承载消息发送、流式合并、状态流转、插件生命周期、递归 request 等核心流程
  • 新增 MessageStateAdapter 抽象,用于解耦不同运行时的状态实现
  • 新增 native / vue 两个 adapter
    • native:基于 mutate + subscribe
    • vue:基于 ref/reactive/computed
  • 新增 core 侧插件实现
    • thinkingPlugin
    • lengthPlugin
    • toolPlugin
  • Vue 侧插件逐步改为薄包装或桥接层,复用 core 插件能力
  • useMessage 改为基于 createMessageEngine + createVueMessageAdapter 实现
  • 保留 Vue 侧原有 API 形态与响应式使用体验
  • 移除 fallbackRolePlugin 相关逻辑与文档残留
  • 补充 native / vue / useMessage 测试,以及通用 mockResponseProvider

设计说明

  • core 负责统一消息处理流程,不依赖具体框架
  • adapter 负责状态宿主和运行时能力注入
  • Vue 场景下,消息对象通过 adapter 的 createMessage 进入响应式系统,保证对 message.contentmessage.state 等字段的原地修改能够被正确追踪
  • 插件在 core 中创建消息时,也统一通过 createMessage 构造,避免 raw object 和响应式对象混用

变更结果

  • message 能力不再绑定 Vue,可用于前后端多平台场景
  • 核心流程只维护一份,减少不同平台实现分叉
  • Vue useMessage 迁移到 core 后,仍然保留原有响应式体验
  • 插件体系开始向 core 收敛,后续扩展新平台成本更低

测试

已补充并通过以下测试:

  • src/message/test/native.test.ts
  • src/message/test/vue.test.ts
  • src/vue/message/useMessage.test.ts

Core 使用示例

import type { MessageRequestBody } from '@opentiny/tiny-robot-kit/core'
import { createMessageEngine, createNativeMessageAdapter } from '@opentiny/tiny-robot-kit/core'

const adapter = createNativeMessageAdapter()

const engine = createMessageEngine(adapter, {
  responseProvider: async function* (requestBody: MessageRequestBody) {
    yield {
      id: 'mock-1',
      object: 'chat.completion.chunk',
      created: Math.floor(Date.now() / 1000),
      model: 'mock',
      choices: [
        {
          index: 0,
          delta: { role: 'assistant', content: 'Hello' },
          finish_reason: null,
        },
      ],
    }

    yield {
      id: 'mock-2',
      object: 'chat.completion.chunk',
      created: Math.floor(Date.now() / 1000),
      model: 'mock',
      choices: [
        {
          index: 0,
          delta: { content: ' world' },
          finish_reason: 'stop',
        },
      ],
    }
  },
})

engine.subscribe('messages', (state) => {
  console.log(state.messages)
})

await engine.sendMessage('Hi')

说明

这次 PR 主要完成了 message core 抽取与 Vue useMessage 迁移。后续如果继续推进,剩余工作会主要集中在:

  • 进一步收敛 Vue 插件桥接实现
  • 完善 core API 文档
  • 继续补充多平台接入示例

Summary by CodeRabbit

  • New Features

    • New message engine with plugin system, native and Vue adapters, expanded core exports, built-in plugins for thinking, length-based continuation, and tool call handling; mock response providers for testing.
  • Documentation

    • Migration and docs updated: simplified default plugins (removed role-fallback) and unified responseProvider typing.
  • Tests

    • Added extensive Vitest suites covering engine, adapters, and Vue integration.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 15, 2026

Walkthrough

Adds a shared message engine (createMessageEngine), Vue and native message adapters, core types and plugins (length, thinking, tool), test utilities and suites, documentation/type updates (including removal of fallbackRolePlugin), new exports including a ./core entry, and several utilities (mock providers, subscription controller, delta combiner).

Changes

Cohort / File(s) Summary
Documentation
docs/src/migration/use-message-migration.md, docs/src/tools/message.md
Removed fallbackRolePlugin from default plugin lists; updated docs to list thinkingPlugin and lengthPlugin as defaults; replaced inline responseProvider signature with ResponseProvider type; updated tool plugin callback/context docs.
Package config & tooling
packages/kit/package.json, packages/kit/vitest.config.ts
Added exports map including ./core subpath (types/import/require entries), added Vitest scripts and deps, and added Vitest config targeting src/**/*.test.ts with Node runtime.
Public barrels & re-exports
packages/kit/src/core.ts, packages/kit/src/message/adapters/index.ts, packages/kit/src/message/core/index.ts, packages/kit/src/message/plugins/index.ts
Added core barrel src/core.ts; expanded adapter index to re-export native and vue; re-exported createMessageEngine and core plugins/utilities to broaden public API.
Core types & utilities
packages/kit/src/message/types.ts, packages/kit/src/message/utils.ts, packages/kit/src/message/adapters/shared.ts, packages/kit/src/types.ts
Introduced full message-engine type surface (ResponseProvider, engine/plugin/adapters, runtime types); renamed combileDeltaDatacombineDeltaData; added createStateSubscriptionController; changed one import to type-only.
Engine implementation
packages/kit/src/message/core/engine.ts
New createMessageEngine implementing turn orchestration, plugin pipeline composition (name dedupe), streaming response handling, request lifecycle hooks (onTurnStart/Before/After/Completion/Error/Finally), abort handling, recursive requestNext support, and public methods (sendMessage/send/abort/setResponseProvider).
Adapters
packages/kit/src/message/adapters/native.ts, packages/kit/src/message/adapters/vue.ts
Added native in-memory adapter with initialize/getState/mutate/subscribe; added Vue adapter returning reactive refs/computeds and bridging reactive state to the engine.
Core plugins
packages/kit/src/message/plugins/lengthPlugin.ts, packages/kit/src/message/plugins/thinkingPlugin.ts, packages/kit/src/message/plugins/toolPlugin.ts
Added core plugins: lengthPlugin (auto-continue on truncated responses), thinkingPlugin (track reasoning_content → message.state.thinking/open during streaming), and toolPlugin (tool-call lifecycle, streaming tool outputs, call status updates, and integration points for tool execution).
Vue plugin refactor & removals
packages/kit/src/vue/message/plugins/*, packages/kit/src/vue/message/types.ts
Removed fallbackRolePlugin; refactored Vue plugins to delegate to core plugin factories via __corePluginFactory, introduced Vue-specific plugin context interfaces, and replaced inline responseProvider signature with shared ResponseProvider type.
useMessage refactor & tests
packages/kit/src/vue/message/useMessage.ts, packages/kit/src/vue/message/useMessage.test.ts, packages/kit/src/message/test/*, packages/kit/src/message/test/mockResponseProvider.ts, packages/kit/src/vue/message/mockResponseProvider.ts
Refactored useMessage to wrap the shared engine + adapter (removed local orchestration); added extensive tests and mock response providers validating streaming, plugins, abort behavior, and Vue reactivity.
Tests & config
packages/kit/vitest.config.ts, test files under packages/kit/src/**/test/*.ts
Added Vitest config and numerous new test suites for native/vue adapters, engine behavior, plugins, and useMessage integration.
Small exports & renames
packages/kit/src/core.ts, packages/kit/src/message/utils.ts
Exported helper utilities (combineDeltaData, normalizeToAsyncGenerator) and renamed delta combiner for consistent naming.

Sequence Diagram(s)

sequenceDiagram
    participant Client as UI / useMessage
    participant Engine as createMessageEngine
    participant Adapter as MessageStateAdapter
    participant Provider as ResponseProvider
    participant Plugins as Plugin Pipeline

    Client->>Engine: sendMessage(content)
    Engine->>Adapter: mutate(append user message)
    Engine->>Plugins: onTurnStart
    Engine->>Plugins: onBeforeRequest(requestBody)
    Engine->>Provider: responseProvider(requestBody, signal)
    loop stream chunks
        Provider->>Engine: yield chunk
        Engine->>Adapter: mutate(update assistant message)
        Engine->>Plugins: onCompletionChunk(chunk, context)
    end
    Engine->>Plugins: onAfterRequest(context)
    Plugins-->>Engine: may append messages or call requestNext
    Engine->>Client: final state / publish updates
Loading
sequenceDiagram
    participant Engine as createMessageEngine
    participant Provider as ResponseProvider
    participant ToolPlugin as toolPlugin
    participant Adapter as MessageStateAdapter

    Engine->>Provider: stream completion (may include tool_calls)
    Provider->>Engine: yields chunk with tool_calls
    Engine->>ToolPlugin: onAfterRequest(detect tool_calls)
    ToolPlugin->>Adapter: append tool messages
    ToolPlugin->>ToolPlugin: callTool (per tool_call) -> stream tool output
    ToolPlugin->>Adapter: stream tool outputs into tool message (update metadata)
    ToolPlugin->>Engine: notify completion / requestNext as needed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I hopped through engine, plugin, and state,

Streams of chunks made messages update,
Adapters in Vue and native land,
Tools and thinking now hand-in-hand,
A little rabbit applauds this grand update.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(message): extract core engine and migrate useMessage to adapters' accurately summarizes the main change: extracting a core message engine and migrating the Vue useMessage implementation to use adapters.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 15, 2026

✅ Preview build completed successfully!

Click the image above to preview.
Preview will be automatically removed when this PR is closed.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 15, 2026

@gene9831 gene9831 marked this pull request as ready for review April 22, 2026 09:38
@gene9831 gene9831 changed the title feat(message-engine): implement message engine core functionality feat(message): extract core engine and migrate useMessage to adapters Apr 22, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (6)
packages/kit/src/message/core/index.ts (1)

1-1: Consider also re-exporting core types from this barrel.

Consumers of ./core barrel (via src/core.ts) already get types through ./message/types, but this sub-barrel only exports the factory. If you intend message/core/index.ts to be a standalone entrypoint, also re-exporting engine-related types (e.g., MessageEngine, CreateMessageEngineOptions) from ./engine here would avoid forcing callers to reach into ./engine directly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kit/src/message/core/index.ts` at line 1, The barrel currently only
re-exports createMessageEngine; export the engine-related types as well so
consumers can import them from this sub-barrel. Update message/core/index.ts to
re-export MessageEngine and CreateMessageEngineOptions (and any other exported
types from ./engine) alongside createMessageEngine so callers don't need to
import directly from './engine'.
packages/kit/package.json (1)

34-48: Minor: consider dropping legacy top-level main/module/types or keeping them consistent with exports.

With the exports map now defined, modern tooling will prefer it over main/module/types. The legacy fields at lines 34-36 still point at dist/index.* so they're consistent, but they're now largely redundant. Keeping them is fine for older bundlers; just something to track for a future cleanup. No change required.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kit/package.json` around lines 34 - 48, The package.json currently
defines legacy top-level fields "main", "module", and "types" that duplicate the
newer "exports" map; either remove these legacy fields ("main", "module",
"types") to avoid redundancy, or ensure they remain perfectly consistent with
the "exports" entries (pointing to dist/index.js, dist/index.mjs,
dist/index.d.ts respectively) so tooling that still reads them sees the same
entrypoints as the "exports" map.
packages/kit/src/message/adapters/index.ts (1)

1-2: Vue adapter leaks into native-only consumers via this barrel.

Because src/core.ts does export * from './message/adapters', any consumer importing from @opentiny/tiny-robot-kit/core (even for native-only usage) transitively pulls in ./vue, which imports from vue. With sideEffects: false and tree-shaking this is usually fine, but it couples the core subpath to the vue peer dependency. Consider splitting into ./core/native and ./core/vue subpath exports so a pure-native consumer never needs vue installed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kit/src/message/adapters/index.ts` around lines 1 - 2, The barrel
index currently re-exports both './native' and './vue' (export * from
'./native'; export * from './vue'), which causes Vue to be pulled into
native-only consumers via core's export of the adapters; to fix, split the
adapters barrel into distinct subpath exports (e.g., keep an adapters/native
index that exports './native' and create adapters/vue that exports './vue'),
update the top-level re-exports (e.g., the symbols exported from core.ts) to
only re-export the native adapters (so core continues to export adapters/native
but not adapters/vue), and leave a separate public subpath for the vue adapter
so consumers that need Vue can import from the explicit vue subpath; ensure
filenames/exports referencing 'native' and 'vue' match the new module names so
tree-shaking and peer-dependency boundaries are preserved.
packages/kit/src/vue/message/plugins/lengthPlugin.ts (1)

5-21: Consider centralizing VuePluginRuntime and __corePluginFactory typing.

The VuePluginRuntime type and the __corePluginFactory field are duplicated verbatim across vue/message/plugins/{thinkingPlugin,lengthPlugin,toolPlugin}.ts, and the shape is also assumed by the useMessage runtime on the consumer side. Hoisting this into a shared module (e.g. packages/kit/src/vue/message/plugins/runtime.ts or the Vue types file) would remove the as UseMessagePlugin casts needed to smuggle in the __corePluginFactory property and keep the bridging contract single-sourced.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kit/src/vue/message/plugins/lengthPlugin.ts` around lines 5 - 21,
The VuePluginRuntime type and the __corePluginFactory field are duplicated;
extract the VuePluginRuntime type (including the createCorePlugin signature) and
a shared interface/type for plugins that expose __corePluginFactory into a
single shared module, then update lengthPlugin (and the sibling files
thinkingPlugin and toolPlugin) to import those shared types and use them
directly so you can return the plugin object without using the as
UseMessagePlugin cast; specifically update the lengthPlugin function that
constructs lengthPlugin and its call to createCoreLengthPlugin to reference the
shared runtime type and shared plugin interface so the bridging contract matches
the useMessage runtime.
packages/kit/src/vue/message/types.ts (1)

80-83: Align Vue ResponseProvider with core type definition.

Vue's ResponseProvider (line 80) lacks the generic constraint T extends ChatCompletion | ChatCompletionChunk and defaults to ChatCompletion only. Core's ResponseProvider (packages/kit/src/message/types.ts:38) constrains the type parameter and defaults to the union. This divergence means streaming providers yielding ChatCompletionChunk without explicit generics will silently use the wrong type default, bypassing type safety intended for the primary streaming use case.

Consider updating the Vue definition to match the core type signature, or re-exporting the core type for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kit/src/vue/message/types.ts` around lines 80 - 83, The Vue type
ResponseProvider currently defaults T to ChatCompletion and lacks the generic
constraint allowing ChatCompletionChunk; update ResponseProvider<T =
ChatCompletion | ChatCompletionChunk> to constrain T extends ChatCompletion |
ChatCompletionChunk (matching the core signature) or re-export the core
ResponseProvider type to ensure consistency; change the declaration for
ResponseProvider (which accepts MessageRequestBody and AbortSignal) to use the
constrained generic so streaming providers that yield ChatCompletionChunk use
the correct default and type checks.
packages/kit/src/vue/message/mockResponseProvider.ts (1)

11-16: Keep mock completions checked against the SDK shape.

createMockCompletion is meant to produce a ChatCompletion, but the unannotated return object plus finish_reason?: string | null lets invalid fixture shapes compile. Pin the helper to the OpenAI type so tests fail when the mock drifts from the real response contract.

Type the mock fixture against ChatCompletion
-import type { ChatCompletionChunk } from 'openai/resources/index'
+import type { ChatCompletion, ChatCompletionChunk } from 'openai/resources/index'
@@
-  finish_reason?: string | null
+  finish_reason?: ChatCompletion.Choice['finish_reason']
@@
-function createMockCompletion(step: MockResponseStep, index: number) {
+function createMockCompletion(step: MockResponseStep, index: number): ChatCompletion {

Also applies to: 46-68

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kit/src/vue/message/mockResponseProvider.ts` around lines 11 - 16,
The mock fixtures are not strongly typed against the real OpenAI response shape;
update MockResponseStep and the createMockCompletion helper to use the OpenAI
ChatCompletion types so invalid shapes fail to compile. Specifically, change the
finish_reason and other fields to match ChatCompletion (for example using
ChatCompletion and ChatCompletion['choices'][number] types) and annotate
createMockCompletion to return ChatCompletion; ensure onRequest and tool_calls
still match their expected types while aligning the mock fields with
ChatCompletion's exact property types.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/kit/src/core.ts`:
- Line 5: The export name "combileDeltaData" is misspelled; rename the function
symbol in the implementation to combineDeltaData (update its declaration in
message utils where combileDeltaData is declared) and update all places that
import or call it: change exports in core.ts to export { combineDeltaData,
normalizeToAsyncGenerator }, update imports in core/engine.ts and
plugins/toolPlugin.ts to import combineDeltaData instead of combileDeltaData,
and adjust any internal usages inside message/utils.ts (previous
combileDeltaData references) to the new name so all callers compile.

In `@packages/kit/src/message/adapters/native.ts`:
- Around line 19-30: The initialize function silently no-ops when called after
initial setup, which can hide binding/usage bugs; change initialize to throw an
error if initialized is already true (instead of returning) so repeated calls
immediately surface misuse — update the initialize implementation (check the
initialized flag and throw a descriptive Error mentioning initialize/initialized
and the adapter) and ensure behavior remains consistent with getState() and
mutate() which already throw when uninitialized.
- Around line 45-66: notifyListeners currently iterates the live Set listeners
which is not reentrancy-safe (a listener can subscribe/unsubscribe during
notification); fix by iterating a snapshot copy instead: capture snapshot =
getState() and make a stable array copy of listeners (e.g.,
Array.from(listeners) or [...listeners]) before the for loop, then iterate that
copy while preserving the existing kind-filtering logic (use entry.kinds,
entry.listener, MessageUpdateKinds and the existing kinds Set) so mutations to
the original listeners Set during notification won't affect this iteration.

In `@packages/kit/src/message/adapters/vue.ts`:
- Around line 84-120: The recipe can call skipNotify to defer reactive
notifications, but the current code returns early when notifySkipped is true
which prevents emitting a single atomic snapshot; change the flow so after
calling recipe(draft, skipNotify) you detect notifySkipped and instead of
returning immediately you clear notifySkipped and trigger one consolidated
notification (e.g., touch/assign the refs once) so subscribers to requestState
see a single atomic update for requestState+processingState; update the block
around recipe, skipNotify, notifySkipped and the later updateKinds handling
(symbols: recipe, skipNotify, notifySkipped, draft.requestState/requesting,
draft.processingState, messages and updateKinds) to perform one post-recipe
dispatch rather than letting Vue watchers run twice.

In `@packages/kit/src/message/core/engine.ts`:
- Around line 187-192: The call to responseProvider(requestBody, abortSignal)
and subsequent iteration over normalizeToAsyncGenerator(result) can hang if the
provider or generator ignores the abortSignal; wrap the provider invocation and
the async-iteration in abort-safe races (e.g., use a makeAbortable helper or
equivalent) so the provider call (responseProvider) is raced against abortSignal
and the async generator returned by normalizeToAsyncGenerator(result) is
consumed via an abortable wrapper (or by racing each iterator read against
abortSignal). Apply the same pattern for the other streaming block (the code
handling the second provider/stream around the other streaming loop) so both
provider invocation and each async iterator read are abort-safe.

In `@packages/kit/src/message/plugins/thinkingPlugin.ts`:
- Line 15: Replace the fallback using logical OR when computing
reasoning_content so empty strings are treated as valid; in thinkingPlugin.ts
update the assignment for the reasoning_content variable (currently using
c?.message?.reasoning_content || c?.delta?.reasoning_content) to use nullish
coalescing (??) instead, i.e. fall back to c?.delta?.reasoning_content only when
message.reasoning_content is null or undefined.
- Around line 45-64: The onTurnEnd guard is comparing the current thinking value
against itself so it often short-circuits and fails to reset thinking/open;
change the condition in onTurnEnd to detect whether we need to force-reset to
false by comparing the current state to the target state (thinking: false, open:
false) instead of Boolean(lastMessage.state.thinking). Specifically, inside
onTurnEnd, use stateShouldUpdate(lastMessage.state, { thinking: false, open:
false }) (or otherwise compute current !== target) before calling
mutate('messages') and then set lastMessage.state.thinking = false and
lastMessage.state.open = false; this ensures non-streamed or aborted turns get
their thinking/open flags cleared.

In `@packages/kit/src/message/plugins/toolPlugin.ts`:
- Around line 296-317: The current check uses toolMessage.content.length === 0
so when earlier object-chunks are merged (producing strings like "{}" or
"{\"partial\":...}") a failure will leave visible empty/partial JSON instead of
the failure text; fix by tracking whether any successful chunk was actually
produced (e.g., add a boolean flag producedChunk on the toolCall or toolMessage
and set it inside the chunk-merge code where mutate(...) updates toolMessage
content), then in the catch use that flag (or normalize content to treat "" and
"{}" as empty) before assigning toolCallFailedContent; keep toolCallEnd, mutate
and toolMessage assignments behavior otherwise.
- Around line 213-222: The optional chaining on getTools is dead because
getTools is required in the options type; call it directly instead of using
getTools?.() in the onBeforeRequest handler, i.e. replace getTools?.() with
getTools(), and keep the rest of the logic that assigns requestBody.tools and
returns restOptions.onBeforeRequest?.(context); alternatively, if the intent was
to allow missing getTools, make getTools optional in the options type (and where
it’s destructured) to match the runtime check.
- Around line 296-324: The code calls requestNext() unconditionally after
awaiting toolCallPromises, which can trigger a recursive executeRequest with an
already-aborted abortSignal; update the flow to guard the requestNext()
invocation by checking abortSignal.aborted (or a local cancelled flag set in the
catch branch) and only call requestNext() when not aborted, ensuring
toolCallEnd(...) stays as-is for cancellation/failed cases and avoiding phantom
follow-up requests.

In `@packages/kit/src/message/test/mockResponseProvider.ts`:
- Around line 12-44: In mockStreamOneAssistantReplyWithDelay update the
signature to mark delay optional (e.g., { abortSignal, delay?: number }) and
replace the plain setTimeout sleep with an abort-aware race: create a Promise
that sets a timeout and also registers an abortSignal 'abort' listener (with {
once: true }) that clears the timeout and rejects with an AbortError so the
generator will immediately throw if aborted during the delay; ensure you remove
the event listener/clear the timeout on resolve/reject to avoid leaks and keep
the existing top-of-loop abort check as well.

---

Nitpick comments:
In `@packages/kit/package.json`:
- Around line 34-48: The package.json currently defines legacy top-level fields
"main", "module", and "types" that duplicate the newer "exports" map; either
remove these legacy fields ("main", "module", "types") to avoid redundancy, or
ensure they remain perfectly consistent with the "exports" entries (pointing to
dist/index.js, dist/index.mjs, dist/index.d.ts respectively) so tooling that
still reads them sees the same entrypoints as the "exports" map.

In `@packages/kit/src/message/adapters/index.ts`:
- Around line 1-2: The barrel index currently re-exports both './native' and
'./vue' (export * from './native'; export * from './vue'), which causes Vue to
be pulled into native-only consumers via core's export of the adapters; to fix,
split the adapters barrel into distinct subpath exports (e.g., keep an
adapters/native index that exports './native' and create adapters/vue that
exports './vue'), update the top-level re-exports (e.g., the symbols exported
from core.ts) to only re-export the native adapters (so core continues to export
adapters/native but not adapters/vue), and leave a separate public subpath for
the vue adapter so consumers that need Vue can import from the explicit vue
subpath; ensure filenames/exports referencing 'native' and 'vue' match the new
module names so tree-shaking and peer-dependency boundaries are preserved.

In `@packages/kit/src/message/core/index.ts`:
- Line 1: The barrel currently only re-exports createMessageEngine; export the
engine-related types as well so consumers can import them from this sub-barrel.
Update message/core/index.ts to re-export MessageEngine and
CreateMessageEngineOptions (and any other exported types from ./engine)
alongside createMessageEngine so callers don't need to import directly from
'./engine'.

In `@packages/kit/src/vue/message/mockResponseProvider.ts`:
- Around line 11-16: The mock fixtures are not strongly typed against the real
OpenAI response shape; update MockResponseStep and the createMockCompletion
helper to use the OpenAI ChatCompletion types so invalid shapes fail to compile.
Specifically, change the finish_reason and other fields to match ChatCompletion
(for example using ChatCompletion and ChatCompletion['choices'][number] types)
and annotate createMockCompletion to return ChatCompletion; ensure onRequest and
tool_calls still match their expected types while aligning the mock fields with
ChatCompletion's exact property types.

In `@packages/kit/src/vue/message/plugins/lengthPlugin.ts`:
- Around line 5-21: The VuePluginRuntime type and the __corePluginFactory field
are duplicated; extract the VuePluginRuntime type (including the
createCorePlugin signature) and a shared interface/type for plugins that expose
__corePluginFactory into a single shared module, then update lengthPlugin (and
the sibling files thinkingPlugin and toolPlugin) to import those shared types
and use them directly so you can return the plugin object without using the as
UseMessagePlugin cast; specifically update the lengthPlugin function that
constructs lengthPlugin and its call to createCoreLengthPlugin to reference the
shared runtime type and shared plugin interface so the bridging contract matches
the useMessage runtime.

In `@packages/kit/src/vue/message/types.ts`:
- Around line 80-83: The Vue type ResponseProvider currently defaults T to
ChatCompletion and lacks the generic constraint allowing ChatCompletionChunk;
update ResponseProvider<T = ChatCompletion | ChatCompletionChunk> to constrain T
extends ChatCompletion | ChatCompletionChunk (matching the core signature) or
re-export the core ResponseProvider type to ensure consistency; change the
declaration for ResponseProvider (which accepts MessageRequestBody and
AbortSignal) to use the constrained generic so streaming providers that yield
ChatCompletionChunk use the correct default and type checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 773679df-9210-4568-b7b1-bb43943228c3

📥 Commits

Reviewing files that changed from the base of the PR and between 6a8bf29 and e1e806c.

📒 Files selected for processing (29)
  • docs/src/migration/use-message-migration.md
  • docs/src/tools/message.md
  • packages/kit/package.json
  • packages/kit/src/core.ts
  • packages/kit/src/message/adapters/index.ts
  • packages/kit/src/message/adapters/native.ts
  • packages/kit/src/message/adapters/vue.ts
  • packages/kit/src/message/core/engine.ts
  • packages/kit/src/message/core/index.ts
  • packages/kit/src/message/plugins/index.ts
  • packages/kit/src/message/plugins/lengthPlugin.ts
  • packages/kit/src/message/plugins/thinkingPlugin.ts
  • packages/kit/src/message/plugins/toolPlugin.ts
  • packages/kit/src/message/test/mockResponseProvider.ts
  • packages/kit/src/message/test/native.test.ts
  • packages/kit/src/message/test/vue.test.ts
  • packages/kit/src/message/types.ts
  • packages/kit/src/message/utils.ts
  • packages/kit/src/types.ts
  • packages/kit/src/vue/message/mockResponseProvider.ts
  • packages/kit/src/vue/message/plugins/fallbackRolePlugin.ts
  • packages/kit/src/vue/message/plugins/index.ts
  • packages/kit/src/vue/message/plugins/lengthPlugin.ts
  • packages/kit/src/vue/message/plugins/thinkingPlugin.ts
  • packages/kit/src/vue/message/plugins/toolPlugin.ts
  • packages/kit/src/vue/message/types.ts
  • packages/kit/src/vue/message/useMessage.test.ts
  • packages/kit/src/vue/message/useMessage.ts
  • packages/kit/vitest.config.ts
💤 Files with no reviewable changes (2)
  • packages/kit/src/vue/message/plugins/index.ts
  • packages/kit/src/vue/message/plugins/fallbackRolePlugin.ts

Comment thread packages/kit/src/core.ts Outdated
Comment thread packages/kit/src/message/adapters/native.ts
Comment thread packages/kit/src/message/adapters/native.ts Outdated
Comment thread packages/kit/src/message/adapters/vue.ts Outdated
Comment thread packages/kit/src/message/core/engine.ts
Comment thread packages/kit/src/message/plugins/thinkingPlugin.ts
Comment thread packages/kit/src/message/plugins/toolPlugin.ts
Comment thread packages/kit/src/message/plugins/toolPlugin.ts
Comment thread packages/kit/src/message/plugins/toolPlugin.ts
Comment thread packages/kit/src/message/test/mockResponseProvider.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
packages/kit/src/message/core/engine.ts (1)

187-192: ⚠️ Potential issue | 🟠 Major

Race provider calls and stream reads against abort.

If responseProvider or the async iterator ignores abortSignal, abort() can wait forever for isProcessing to become false. The provider result and each iterator .next() need to be wrapped with makeAbortable.

Abort-safe stream consumption
-    const result = responseProvider(requestBody, abortSignal)
+    const result = makeAbortable(Promise.resolve(responseProvider(requestBody, abortSignal)), abortSignal)
     const chunks = normalizeToAsyncGenerator(result)
 
     let lastChoice: ChatCompletionChoice | undefined = undefined
 
-    for await (const chunk of chunks) {
+    while (true) {
+      const { value: chunk, done } = await makeAbortable(chunks.next(), abortSignal)
+      if (done) {
+        break
+      }
+
       setRequestState('processing', 'completing')

Also applies to: 446-459

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kit/src/message/core/engine.ts` around lines 187 - 192, The call to
responseProvider and the async-iterator consumption in the loop are not
abort-safe: wrap the initial responseProvider(requestBody, abortSignal)
invocation with makeAbortable so it rejects if abortSignal fires, and wrap each
await on the iterator (each .next()/for-await step) with makeAbortable as well,
passing the same abortSignal; update the usage around
normalizeToAsyncGenerator(result) and the for-await (chunks) consumption to use
the abortable-wrapped promises so abort() cannot hang waiting for isProcessing,
and apply the same change to the other similar block that consumes the provider
stream (the block around the later iterator usage).
🧹 Nitpick comments (1)
packages/kit/src/message/utils.ts (1)

133-133: Deprecation alias is optional but recommended for public APIs.

The verification confirms no internal references to the old misspelled name exist in the codebase, so this rename poses no risk to internal consumers. However, if this is a public API consumed externally, adding a deprecated alias is still a best practice for smoother adoption across releases:

Optional compatibility alias
 export const combineDeltaData = (target: Record<string, any>, source: Record<string, any>) => {
   for (const [sourceKey, sourceValue] of Object.entries(source)) {
     const targetValue = target[sourceKey]
@@
 
   return target
 }
+
+/** `@deprecated` Use `combineDeltaData` instead. */
+export const combileDeltaData = combineDeltaData
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/kit/src/message/utils.ts` at line 133, Add a deprecated
compatibility alias for the old misspelled export by exporting the original name
(e.g., the old misspelled symbol) and pointing it to the new function
combineDeltaData; add a JSDoc `@deprecated` tag on the alias that references
combineDeltaData so external consumers see the deprecation notice while internal
calls keep working. Ensure the alias is exported from the same module and simply
re-exports or references combineDeltaData (no duplicate logic) so callers of the
old name still work.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/kit/src/message/adapters/shared.ts`:
- Around line 20-64: The subscribe flow and notify loop must isolate listener
exceptions so a thrown listener doesn't stop later listeners or bubble into
mutate/engine flows and must ensure subscribe always returns an unsubscribe
handle even if the initial listener call throws; in
createStateSubscriptionController wrap the immediate listener invocation (the
listener(getState()) call in subscribe) in a try/catch that logs or swallows the
error so it cannot propagate, and similarly wrap each entry.listener(snapshot)
call inside notify in a try/catch so one listener's throw does not prevent
others from running; keep adding/removing entries using the existing listeners
Set and do not rethrow errors.

In `@packages/kit/src/message/plugins/toolPlugin.ts`:
- Around line 235-237: The early-return in toolPlugin.ts that checks if
lastChoice?.finish_reason !== 'tool_calls' || !currentMessage.tool_calls?.length
exits before invoking the wrapped restOptions.onAfterRequest, causing
consumer-provided onAfterRequest hooks to be dropped for normal completions;
update this branch (and the similar early-return around the other occurrence
noted) to call the original restOptions.onAfterRequest(with the same
args/context) before returning so the consumer hook still runs even when there
are no tool calls, and ensure you reference the same wrapped function used
elsewhere in toolPlugin (restOptions.onAfterRequest) so behavior remains
consistent.
- Around line 240-273: Wrap the pre-tool hook and each tool async-iterator read
with the existing makeAbortable so they respect the provided abortSignal: race
the beforeCallTools(...) call with makeAbortable using context.abortSignal; when
you call callTool(...) and get the normalized iterator
(normalizeToAsyncGenerator), don't await the whole generator directly—wrap each
awaited step (or the iterator.next() Promise) with makeAbortable using
context.abortSignal so the for-await loop can be aborted; keep using
toolCallStart, appendMessage and toolCallPromises but ensure both
beforeCallTools and per-chunk reads are made abortable via makeAbortable.

---

Duplicate comments:
In `@packages/kit/src/message/core/engine.ts`:
- Around line 187-192: The call to responseProvider and the async-iterator
consumption in the loop are not abort-safe: wrap the initial
responseProvider(requestBody, abortSignal) invocation with makeAbortable so it
rejects if abortSignal fires, and wrap each await on the iterator (each
.next()/for-await step) with makeAbortable as well, passing the same
abortSignal; update the usage around normalizeToAsyncGenerator(result) and the
for-await (chunks) consumption to use the abortable-wrapped promises so abort()
cannot hang waiting for isProcessing, and apply the same change to the other
similar block that consumes the provider stream (the block around the later
iterator usage).

---

Nitpick comments:
In `@packages/kit/src/message/utils.ts`:
- Line 133: Add a deprecated compatibility alias for the old misspelled export
by exporting the original name (e.g., the old misspelled symbol) and pointing it
to the new function combineDeltaData; add a JSDoc `@deprecated` tag on the alias
that references combineDeltaData so external consumers see the deprecation
notice while internal calls keep working. Ensure the alias is exported from the
same module and simply re-exports or references combineDeltaData (no duplicate
logic) so callers of the old name still work.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7d4c5186-cc21-4cd3-af8b-c6991d6a8733

📥 Commits

Reviewing files that changed from the base of the PR and between e1e806c and c8da78e.

📒 Files selected for processing (9)
  • packages/kit/src/core.ts
  • packages/kit/src/message/adapters/native.ts
  • packages/kit/src/message/adapters/shared.ts
  • packages/kit/src/message/adapters/vue.ts
  • packages/kit/src/message/core/engine.ts
  • packages/kit/src/message/plugins/toolPlugin.ts
  • packages/kit/src/message/test/native.test.ts
  • packages/kit/src/message/test/vue.test.ts
  • packages/kit/src/message/utils.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/kit/src/core.ts
  • packages/kit/src/message/adapters/native.ts

Comment thread packages/kit/src/message/adapters/shared.ts
Comment thread packages/kit/src/message/plugins/toolPlugin.ts
Comment thread packages/kit/src/message/plugins/toolPlugin.ts
…ate and update state management

feat(toolPlugin): improve tool handling by ensuring meaningful results and updating metadata
fix(mockResponseProvider): make delay parameter optional in mock stream function
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/kit/src/message/plugins/toolPlugin.ts`:
- Around line 283-297: The handler can drop earlier string chunks when a later
object chunk fails JSON.parse; update the logic in the tool chunk merge to
preserve prior string content: when chunk is an object but toolMessage.content
is currently a string (or content parse fails), treat the existing content as
text (e.g., set parsedContent to { text: existingString } or otherwise wrap it)
instead of defaulting to {}; then call combineDeltaData(parsedContent, chunk)
and stringify the result; add an explicit flag or use toolMessage.content type
checks to track whether the stream is in "string mode" vs "object mode" so
future chunks are merged consistently (refer to toolMessage, chunk,
combineDeltaData and the JSON.parse block).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 68fce8f1-3ee7-4616-8fa3-b56c3fa2b825

📥 Commits

Reviewing files that changed from the base of the PR and between c8da78e and e5e28db.

📒 Files selected for processing (3)
  • packages/kit/src/message/plugins/thinkingPlugin.ts
  • packages/kit/src/message/plugins/toolPlugin.ts
  • packages/kit/src/message/test/mockResponseProvider.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/kit/src/message/plugins/thinkingPlugin.ts
  • packages/kit/src/message/test/mockResponseProvider.ts

Comment thread packages/kit/src/message/plugins/toolPlugin.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant