Skip to content

feat(tool): add pause/resume support for tool calls#361

Open
gene9831 wants to merge 5 commits into
opentiny:developfrom
gene9831:feat/tool-call-pause
Open

feat(tool): add pause/resume support for tool calls#361
gene9831 wants to merge 5 commits into
opentiny:developfrom
gene9831:feat/tool-call-pause

Conversation

@gene9831

@gene9831 gene9831 commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Kit ToolCall Pause/Resume

Summary

本 PR 为 kit 侧 toolCall 增加 pause/resume 能力,支持工具调用在执行前进入等待确认状态,并在外部确认后恢复执行工具调用及后续模型请求。该能力主要用于人工审批、敏感工具确认、二次授权等场景。

Architecture Design

本次 pause/resume 设计围绕三个职责展开:engine 负责状态与生命周期,插件命令通道负责外部恢复入口,toolPlugin 负责工具调用的暂停、恢复与后续请求编排。

整体时序

sequenceDiagram
  participant Engine
  participant Tool as toolPlugin
  participant UI
  participant Model

  Engine->>Model: request messages
  Model-->>Engine: assistant with tool_calls
  Engine->>Tool: onAfterRequest
  Tool->>Tool: shouldPauseToolCall
  alt no pause
    Tool->>Tool: callTool
    Tool->>Engine: requestNext
    Engine->>Model: request with tool results
  else pause
    Tool->>Engine: setRequestState paused
    Engine->>Tool: onTurnPause
    Engine-->>UI: show awaiting approval
    UI->>Engine: runPluginCommand resumeToolCall
    Engine->>Tool: commands.resumeToolCall
    Tool->>Tool: callTool
    Tool->>Engine: requestNext resume
    Engine->>Tool: onTurnResume
    Engine->>Model: request with tool results
  end
  Model-->>Engine: final assistant answer
Loading

Engine 状态机

message engine 新增 paused 请求状态,用于表示当前 turn 并未完成,而是被插件主动暂停。

paused 状态由插件在请求处理中通过上下文 API 主动进入。engine 只负责记录暂停状态、触发生命周期,并允许插件在后续恢复时通过 requestNext({ resume: true }) 重新接回请求流程。

当请求进入 paused 后:

  • 触发 onTurnPause,而不是原来的 onTurnEnd,避免把暂停误判为完整 turn 结束。
  • 后续恢复请求通过 requestNext({ resume: true }) 进入新一轮请求流程。
  • resume 触发的请求只触发 onTurnResume,而不是原来的 onTurnStart,避免重复执行初始化逻辑。

插件命令通道

engine 新增 runPluginCommand(pluginName, commandName, payload),插件可以通过 commands 暴露外部可调用能力。

例如工具调用要求手动确认后再恢复执行。

engine 不提供诸如 pauseToolCall / resumeToolCall 之类的专用 API,而是提供通用的插件命令机制;具体的 resumeToolCall 语义由 toolPlugin 通过 commands 注册和实现:

  • engine 负责命令注册、查找、执行和统一返回结果。
  • plugin 负责解释命令语义。
  • 命令执行时可以 appendMessagesetRequestStaterequestNext,从而安全地触发后续请求流程。
  • processing 中禁止执行插件命令,避免请求流与命令流并发修改消息状态。
flowchart LR
  subgraph PluginRegistration[Plugin registration]
    ToolPlugin[toolPlugin] -->|register commands| CommandRegistry[engine command registry]
    OtherPlugin[other plugin] -->|register commands| CommandRegistry
  end

  UI[Business UI] -->|runPluginCommand| Engine[Message Engine]
  Engine -->|find handler| CommandRegistry
  CommandRegistry -->|execute handler| CommandHandler[plugin command handler]
  CommandHandler -->|appendMessage / setRequestState / requestNext| Engine
Loading

toolPlugin 工具调用编排

toolPlugin 新增 shouldPauseToolCall(toolCall, context) 钩子。模型返回 tool_calls 后,插件会为每个 toolCall 创建 tool message;

如果 shouldPauseToolCall 返回 true,则:

  • 不调用 callTool
  • 将该 toolCall 状态标记为 awaiting-approval
  • 将 engine 请求状态设置为 paused
  • 保留 assistant + tool message 结构,便于 UI 展示待确认状态,也便于恢复时定位上下文。

恢复时通过内置命令:

engine.runPluginCommand('tool', 'resumeToolCall', { toolCallId })

resumeToolCall 会:

  • 在最新的 assistant + 连续 tool message 组里查找待恢复的 toolCall。
  • 校验目标 toolCall 当前必须是 awaiting-approval
  • 将请求状态切到 processing/calling-tools,重新进入 toolPlugin 的工具执行流程。
  • 调用原有 callTool 流程执行工具。
  • 如果仍有其他 toolCall 等待确认,则回到 paused
  • 如果所有 toolCall 都已完成,则调用 requestNext({ resume: true }) 继续请求模型生成最终回复。

Changes

  • 为 message engine 新增 paused 状态、插件命令机制、onTurnPause / onTurnResume 生命周期。
  • toolPlugin 新增 shouldPauseToolCall 与内置 resumeToolCall 命令。
  • Vue useMessage 同步暴露 runPluginCommand,并透传 pause/resume 相关插件能力。
  • 组件侧新增 awaiting-approval toolCall 状态,支持展示待确认工具调用。
  • 补充 native engine 与 Vue useMessage 测试,覆盖暂停、恢复、多 toolCall、重复恢复、从 initialMessages 恢复等场景。

Test Plan

  • pnpm --filter @opentiny/tiny-robot-kit test
  • pnpm --filter @opentiny/tiny-robot-kit type-check

Summary by CodeRabbit

  • New Features

    • Tool calls can pause awaiting approval (new awaiting-approval status) and be resumed; requests may enter a paused state.
    • Plugins can expose and run named commands via a new runPluginCommand API; turn resumption is supported for resumed flows.
  • Chores

    • Added a type-check script and new test utilities for the message engine.

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a06dc7a0-1e43-4976-a11c-af5cfefc6de5

📥 Commits

Reviewing files that changed from the base of the PR and between 9d5cee8 and 037ef89.

📒 Files selected for processing (4)
  • packages/kit/src/message/plugins/toolPlugin.ts
  • packages/kit/src/message/test/native.test.ts
  • packages/kit/src/message/types.ts
  • packages/kit/src/vue/message/types.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/kit/src/vue/message/types.ts
  • packages/kit/src/message/types.ts
  • packages/kit/src/message/test/native.test.ts
  • packages/kit/src/message/plugins/toolPlugin.ts

Walkthrough

Adds pause/resume control for tool calls in the message engine. Plugin commands can now be registered and executed to suspend/resume individual tool calls. The engine lifecycle and tool plugin are refactored to support pausing, awaiting approval, and resuming tool execution via the new plugin command API, with comprehensive test coverage for all scenarios.

Changes

Pause/Resume Tool-Call Flow

Layer / File(s) Summary
Core message engine command and lifecycle contracts
packages/kit/src/message/core/engine.ts, packages/kit/src/message/types.ts
MessageEngine adds runPluginCommand, PluginCommandResult, RequestNextOptions, and engine refactors into runTurnLifecycle with resume support, command registration, and resume-time turn reconstruction.
Vue message hook command and lifecycle contracts
packages/kit/src/vue/message/types.ts, packages/kit/src/vue/message/useMessage.ts
RequestState includes 'paused'; UseMessageReturn exposes runPluginCommand; UseMessagePlugin supports commands, onTurnResume, and onTurnPause; useMessage wires commands and lifecycle hooks to the engine.
Tool plugin pause gate and resumed execution path
packages/kit/src/message/plugins/toolPlugin.ts
Adds optional shouldPauseToolCall hook, processToolCall unified stream handling, per-call awaiting-approval status, pause gating, lazy tool-message recovery on resume, and completion gating until group calls finish.
Message test helper consolidation and factories
packages/kit/src/message/test/helpers.ts, packages/kit/src/vue/message/test/helpers.ts
Consolidates shared test utilities: silentDefaultPlugins, createTestMessageEngine, tool-call helpers, deterministic completion builders, and updated streaming mock shaping to emit ChatCompletion objects.
Test file migration to shared helpers and updated import paths
packages/kit/src/message/test/native.test.ts, packages/kit/src/message/test/vue.test.ts, packages/kit/src/vue/message/test/useMessage.test.ts
Refactors tests to use shared helpers, updates imports, and adds native engine tests for paused/resumed tool-call flows and command behaviors.
Build config, dependencies, and component status updates
packages/kit/package.json, packages/components/vite.config.ts, packages/components/src/bubble/composables/useToolCall.ts
Adds type-check npm script and vite devDependency; Vite config now requires src/<dir>/index.ts for component entrypoints; tool-call status union adds 'awaiting-approval'.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Pauses bloom in tool-call fields,
Commands hop in with gentle wills,
Engine listens, resumes the song,
Awaiting approval won't stay long,
A rabbit nods — the run goes on.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main feature being added: pause/resume support for tool calls, which is the primary focus of all changes across the codebase.
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.

✏️ 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

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

✅ Preview build completed successfully!

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

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

@gene9831 gene9831 marked this pull request as ready for review June 7, 2026 16:27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
AGENTS.md (1)

127-153: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update stale P2 checklist items to match current PR state.

Line 129 and Line 152 still list pause/resume test coverage as pending, but those cases are now implemented in packages/kit/src/message/test/native.test.ts (Line 315-Line 515). Keeping this stale will misdirect next-step tracking.

Suggested doc adjustment
-### P2 - API Completeness / Polish
-
-1. Add focused tests for the pause/resume flow.
-
-   Suggested cases:
-
-   - Tool call pauses and does not call `callTool`.
-   - Paused tool call does not trigger `requestNext`.
-   - Resume calls `callTool`.
-   - Multiple tool calls with mixed paused/running behavior.
-   - All tool calls complete and continue the model request.
-   - Resume works from `initialMessages` after local restore.
+No P2 items are currently open.
@@
-1. P2: add focused pause/resume tests.
+1. Monitor pause/resume behavior and add edge-case tests only if new regressions appear.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AGENTS.md` around lines 127 - 153, Update the P2 checklist in AGENTS.md to
mark the pause/resume test items as completed (remove or change the “pending”
status for the pause/resume cases listed around lines 129 and 152) because the
pause/resume coverage is implemented in the native test suite (the message
native.test.ts tests covering pause/resume flow). Adjust the checklist text to
reference that pause/resume cases are covered by the native message test suite
and remove the stale “add focused pause/resume tests” action item.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/kit/src/message/core/engine.ts`:
- Around line 542-550: The finally block currently clears runtime.currentTurn
(runtime.abortController = null; runtime.currentTurn = []) which causes
command-triggered requestNext() continuations to lose messages queued via
appendMessage; instead, preserve currentTurn when we're about to call
runTurnLifecycle for a continuation. Modify the cleanup so that
runtime.currentTurn is not cleared unconditionally in the finally: either move
the runtime.currentTurn = [] assignment to after the
shouldRequest/runTurnLifecycle branch or add a condition (e.g., only clear when
not shouldRequest or when requestNextOptions?.resume is false). Update the
finally in engine.ts (referencing runtime.abortController, runtime.currentTurn)
and ensure runTurnLifecycle/onTurnStart see the queued messages for
command-driven continuations.

In `@packages/kit/src/message/plugins/toolPlugin.ts`:
- Around line 365-367: resumeToolCall currently invokes processToolCall while
requestState remains 'paused', which lets sendMessage avoid blocking and desyncs
UI/abort handling; before calling processToolCall (or callTool) set the
engine/request state back to 'processing' so the resumed tool call executes
under the correct state. Locate resumeToolCall and update it to flip
requestState from 'paused' to 'processing' (using the same state-management path
you use elsewhere, e.g., requestNext or the getState/setState helpers)
immediately before invoking processToolCall/callTool and ensure this same change
is applied in the analogous block around lines 401-406.
- Around line 375-379: The code currently resumes any tool call found by
toolCallId even if it's already completed; update the guard after finding
toolCall (from assistantMessage.tool_calls?.find(...)) to only proceed when
toolCall.status is the awaiting-approval state (e.g., === 'awaiting_approval' or
whatever enum/value your app uses) and return early for any other status
(including 'success' and 'failed'), so processToolCall() is only re-entered for
calls that are actually awaiting approval.

---

Outside diff comments:
In `@AGENTS.md`:
- Around line 127-153: Update the P2 checklist in AGENTS.md to mark the
pause/resume test items as completed (remove or change the “pending” status for
the pause/resume cases listed around lines 129 and 152) because the pause/resume
coverage is implemented in the native test suite (the message native.test.ts
tests covering pause/resume flow). Adjust the checklist text to reference that
pause/resume cases are covered by the native message test suite and remove the
stale “add focused pause/resume tests” action item.
🪄 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: dd5e7418-91d6-4698-8845-51f380cc2d17

📥 Commits

Reviewing files that changed from the base of the PR and between 045d26b and aca04b8.

📒 Files selected for processing (15)
  • AGENTS.md
  • packages/components/src/bubble/composables/useToolCall.ts
  • packages/components/vite.config.ts
  • packages/kit/package.json
  • packages/kit/src/message/core/engine.ts
  • packages/kit/src/message/plugins/toolPlugin.ts
  • packages/kit/src/message/test/helpers.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/vue/message/test/helpers.ts
  • packages/kit/src/vue/message/test/useMessage.test.ts
  • packages/kit/src/vue/message/types.ts
  • packages/kit/src/vue/message/useMessage.ts
💤 Files with no reviewable changes (1)
  • packages/kit/src/message/test/mockResponseProvider.ts

Comment thread packages/kit/src/message/core/engine.ts Outdated
Comment thread packages/kit/src/message/plugins/toolPlugin.ts Outdated
Comment thread packages/kit/src/message/plugins/toolPlugin.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/kit/src/message/types.ts (1)

198-198: 💤 Low value

Consider type-level enforcement of plugin name requirement for command registration.

The documentation states that only plugins with a name will have their commands registered, but there's no compile-time enforcement. A plugin with commands but no name will silently fail to register.

🔧 Optional: Enforce name requirement with conditional type

While the current approach relies on runtime validation, you could add a conditional type to enforce this at compile time:

export interface MessageEnginePlugin {
  name?: string
  disabled?: boolean | ((context: BasePluginContext) => boolean)
  // Require name when commands are present
  commands?: this extends { name: string }
    ? Record<string, MessagePluginCommandHandler>
    : never
  // ... rest of interface
}

However, this adds complexity and the runtime already handles this case safely by returning an error from runPluginCommand. The current approach favors flexibility.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/kit/src/message/types.ts` at line 198, The interface
MessageEnginePlugin allows plugins to declare commands without a name which
causes silent runtime omission; enforce at the type level by making the commands
property conditional on the presence of a name so TypeScript will error when a
plugin defines commands but lacks a name (update the MessageEnginePlugin
interface and its commands property), while keeping the existing runtime safety
in runPluginCommand unchanged. Refer to the MessageEnginePlugin type and the
commands field and ensure the conditional type ties commands?: Record<string,
MessagePluginCommandHandler> to this extends { name: string } so authors must
provide name when registering commands, then run the type checks and adjust any
plugin implementations that fail.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/kit/src/message/types.ts`:
- Line 198: The interface MessageEnginePlugin allows plugins to declare commands
without a name which causes silent runtime omission; enforce at the type level
by making the commands property conditional on the presence of a name so
TypeScript will error when a plugin defines commands but lacks a name (update
the MessageEnginePlugin interface and its commands property), while keeping the
existing runtime safety in runPluginCommand unchanged. Refer to the
MessageEnginePlugin type and the commands field and ensure the conditional type
ties commands?: Record<string, MessagePluginCommandHandler> to this extends {
name: string } so authors must provide name when registering commands, then run
the type checks and adjust any plugin implementations that fail.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7c8245ab-4b7c-4c07-b31f-7611f40d04cd

📥 Commits

Reviewing files that changed from the base of the PR and between aca04b8 and 9d5cee8.

📒 Files selected for processing (5)
  • 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/types.ts
  • packages/kit/src/vue/message/types.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/kit/src/message/core/engine.ts
  • packages/kit/src/message/plugins/toolPlugin.ts
  • packages/kit/src/vue/message/types.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