Issue #3607044: Append trailing user turn (1.3.x native-SDK path)

Summary

Anthropic rejects a chat request whose last message is not from the user ("the conversation must end with a user message; no assistant prefill"). The AI Agents multi-agent handoff and Drupal Canvas AI can produce a conversation that ends with a trailing assistant turn, producing a generic "Something went wrong" downstream.

Adds a guard in AnthropicProvider::buildMessageCreateParams() after the typed message array is built: if the final message role is not user, append a minimal user turn carrying the text "Continue.". Role=system messages have already been lifted to the top-level system param by extractSystemPrompt() upstream, so only a trailing assistant is possible at this point.

What changed

  • src/Plugin/AiProvider/AnthropicProvider.php — 5-line guard added in buildMessageCreateParams() right after buildMessageContent() returns.
  • tests/src/Unit/Plugin/AiProvider/AnthropicProviderNativeChatTest.php — two new unit tests covering the trailing-assistant (appended) and trailing-user (unchanged) cases against buildMessageCreateParams(). Reuses the existing buildProviderWithCapabilities() + invoke() reflection helpers; no new mock framework.

Relation to MR !30

MR !30 by @RajabNatshah correctly diagnosed this bug and fixes it on the 1.2.x compat-layer stack (override chat() and normalise the ChatInput). That approach doesn't port cleanly to 1.3.x because chat() was fully rewritten in the native-SDK path — parent::chat() no longer resolves to a usable Anthropic implementation.

This MR ports the fix onto the 1.3.x native-SDK path: the guard runs after the typed MessageParam[] array is assembled inside buildMessageCreateParams(), using the same "Continue." nudge RajabNatshah picked. MR !30 stays open — it's still the correct fix for anyone on the 1.2.x compat-layer stack.

How it was verified

Local drupalci-parity gates (runnable locally):

  • phpcs (Drupal + DrupalPractice): clean.
  • phpunit targeted (--filter testBuildParamsAppendsUserWhenTrailingAssistant|testBuildParamsUnchangedWhenTrailingUser): 2 tests, 8 assertions, 12ms — green.
  • phpunit full module unit suite: 86 tests (2 new), 161 assertions, 55ms — green. Zero regressions.
  • php -l on both edited files: no syntax errors.

Deferred to drupalci pipeline: phpstan (workspace env has an unrelated contrib with a broken autoloader), cspell (not installed locally), PHPUnit concurrent variant, opt-in variants.

AI-Generated: Yes

Used Claude Opus 4.8 to draft the 5-line dispatch guard on the 1.3.x path and the two unit tests, reusing the "Continue." nudge that @RajabNatshah picked for MR !30. The bug diagnosis and the prompt-text choice are RajabNatshah's original contributions — this MR ports his work onto the 1.3.x native-SDK stack. Dependencies, logic, security, and GPL compatibility verified. Full contributor responsibility assumed.

Closes #3607044 (once merged alongside MR !30 for 1.2.x, or on its own if the 1.2.x fix ships separately).

Merge request reports

Loading