Skip to content

fix(ai): finish middleware runs on client tool waits#996

Open
JsonKim wants to merge 3 commits into
TanStack:mainfrom
JsonKim:fix/client-tool-wait-lifecycle
Open

fix(ai): finish middleware runs on client tool waits#996
JsonKim wants to merge 3 commits into
TanStack:mainfrom
JsonKim:fix/client-tool-wait-lifecycle

Conversation

@JsonKim

@JsonKim JsonKim commented Jul 26, 2026

Copy link
Copy Markdown

Call terminal middleware hooks when a server invocation pauses for a client tool, ensuring OpenTelemetry spans and duration metrics are finalized.

Defer structured-output finalization until the client result is submitted in the next invocation, with unit and E2E regression coverage.

🎯 Changes

  • Invoke the terminal middleware hook exactly once when an invocation pauses for client-side tool execution or approval.
  • Finalize OpenTelemetry root and iteration spans and record duration and token metrics at the client-tool boundary.
  • Preserve cancellation and error handling through the existing onAbort and onError lifecycle hooks.
  • Avoid native and fallback structured-output finalization while waiting for a client result.
  • Resume structured-output generation in the subsequent invocation after the client tool result is submitted.
  • Add unit coverage for live and pending client-tool waits, OpenTelemetry finalization, and structured-output behavior.
  • Add an E2E scenario verifying that spans finish while waiting and structured output completes only after resolving the client tool.

Tests

  • pnpm --filter @tanstack/ai test:lib --run tests/chat-client-tool-wait.test.ts
  • pnpm --filter @tanstack/ai-e2e test:e2e -- tests/client-tool-wait.spec.ts

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • Bug Fixes

    • Middleware completion callbacks now reliably fire when a chat pauses for client tool results/approvals, without double-firing.
    • Structured output finalization is deferred until the client tool result arrives (or the next invocation), preventing premature structured chunks.
    • Error handling now routes correctly to error callbacks while preserving consistent finish/error behavior.
  • Tests

    • Added expanded unit and e2e coverage for client-tool wait lifecycles, structured output timing, telemetry invariants, and end-to-end “resolve client tool” flows.

Call terminal middleware hooks when a server invocation pauses for a
client tool, ensuring OpenTelemetry spans and duration metrics are
finalized.

Defer structured-output finalization until the client result is
submitted in the next invocation, with unit and E2E regression coverage.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2498c2b9-8dc8-4f2a-9aa5-64a53ac5ded5

📥 Commits

Reviewing files that changed from the base of the PR and between dc8df60 and b8c1128.

📒 Files selected for processing (3)
  • packages/ai/src/activities/chat/index.ts
  • packages/ai/tests/chat-client-tool-wait.test.ts
  • testing/e2e/tests/client-tool-wait.spec.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • testing/e2e/tests/client-tool-wait.spec.ts
  • packages/ai/src/activities/chat/index.ts
  • packages/ai/tests/chat-client-tool-wait.test.ts

📝 Walkthrough

Walkthrough

The chat engine now completes middleware terminal hooks during client-tool waits and postpones structured-output finalization until client results arrive. Unit and end-to-end tests cover lifecycle hooks, telemetry, deferred output, and browser-side tool resolution.

Changes

Client-tool wait lifecycle

Layer / File(s) Summary
Terminal hook dispatch
packages/ai/src/activities/chat/index.ts, .changeset/fuzzy-tools-wait.md
Centralizes terminal middleware dispatch and invokes it during client-tool or approval waits while preserving error details and finish metadata.
Chat lifecycle validation
packages/ai/tests/chat-client-tool-wait.test.ts
Tests terminal hook execution, wait interrupts, telemetry completion, and deferred structured-output finalization.
Structured wait test harness
testing/e2e/src/lib/middleware-test-tools.ts, testing/e2e/fixtures/..., testing/e2e/src/routes/..., testing/e2e/tests/client-tool-wait.spec.ts
Adds the structured client-context tool, browser resolution flow, route wiring, fixture data, and Playwright coverage for final output after resolution.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant TextEngine
  participant ClientTool
  participant Middleware
  Browser->>TextEngine: start structured chat
  TextEngine->>ClientTool: request client context
  TextEngine->>Middleware: run terminal hook
  TextEngine-->>Browser: wait interrupt
  Browser->>ClientTool: resolve client tool
  ClientTool-->>TextEngine: return context
  TextEngine-->>Browser: structured output complete
Loading
🚥 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
Title check ✅ Passed The title is concise and accurately summarizes the main change: middleware runs now finish during client tool waits.
Description check ✅ Passed The description follows the required template and includes changes, checklist items, release impact, and test commands.
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.
✨ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/ai/tests/chat-client-tool-wait.test.ts (1)

83-174: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding an approval-wait analog to these onFinish-once tests.

The changeset/PR explicitly covers hooks finishing "while waiting for client tools or approvals," but this suite only exercises the client-tool-execution wait path (needsClientExecution). runTerminalHook()'s dispatch is shared with the needsApproval branch in emitActionableInterruptBoundary, so a matching test (e.g. a tool with needsApproval: true awaiting an approval decision) would close the coverage gap for the other half of the documented fix.

🤖 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/ai/tests/chat-client-tool-wait.test.ts` around lines 83 - 174, Add a
matching approval-wait lifecycle test alongside the client-tool wait tests,
using a tool configured with needsApproval: true and an unresolved approval
decision. Assert that the terminal middleware’s onFinish is called exactly once
and validates the expected approval-wait finish metadata, reusing the existing
helpers such as createMockAdapter, collectChunks, and expectOnlyFinish.
🤖 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/ai/src/activities/chat/index.ts`:
- Around line 1071-1075: Update the pendingPhase guard in the agent loop to
return after runTerminalHook() when checkForPendingToolCalls() yields either
"stop" or "wait". Preserve the existing terminal handling while preventing
streamModelResponse() from running after a "stop" phase.

In `@testing/e2e/tests/client-tool-wait.spec.ts`:
- Around line 49-71: Update the telemetry assertions in the client-tool wait
test to poll the middleware-test capture endpoint until the expected root span,
client span, and duration record are finalized. Replace the one-shot capture
read with retry logic that waits for ended spans and the histogram before
asserting the exact counts, while preserving the existing testId validation and
response checks.

---

Nitpick comments:
In `@packages/ai/tests/chat-client-tool-wait.test.ts`:
- Around line 83-174: Add a matching approval-wait lifecycle test alongside the
client-tool wait tests, using a tool configured with needsApproval: true and an
unresolved approval decision. Assert that the terminal middleware’s onFinish is
called exactly once and validates the expected approval-wait finish metadata,
reusing the existing helpers such as createMockAdapter, collectChunks, and
expectOnlyFinish.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a3d4dc25-112c-4c0b-b05f-b9489b616d29

📥 Commits

Reviewing files that changed from the base of the PR and between cc88874 and dc8df60.

📒 Files selected for processing (8)
  • .changeset/fuzzy-tools-wait.md
  • packages/ai/src/activities/chat/index.ts
  • packages/ai/tests/chat-client-tool-wait.test.ts
  • testing/e2e/fixtures/middleware-test/structured-client-tool-wait.json
  • testing/e2e/src/lib/middleware-test-tools.ts
  • testing/e2e/src/routes/api.middleware-test.ts
  • testing/e2e/src/routes/middleware-test.tsx
  • testing/e2e/tests/client-tool-wait.spec.ts

Comment thread packages/ai/src/activities/chat/index.ts
Comment thread testing/e2e/tests/client-tool-wait.spec.ts Outdated
JsonKim added 2 commits July 27, 2026 01:11
A restored or retried conversation can start with an unresolved approval or client tool call in its message history. If middleware fails while processing the resulting RUN_FINISHED interrupt boundary, the engine emits RUN_ERROR and returns the pending "stop" phase.

Because run() previously handled only "wait", it entered the agent loop and made another model request after the terminal error.

Reproduction:
1. Start chat() with an unresolved tool call in the initial messages.
2. Configure the tool to require approval or client execution.
3. Throw from middleware while processing the interrupt RUN_FINISHED event.
4. Consume the stream and observe RUN_ERROR followed by an adapter call.

Return immediately for both "stop" and "wait", and add regression coverage verifying that RUN_ERROR invokes onError without another provider call.
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