Add function call resolution loop and withMessages() to PromptBuilder - #273
Add function call resolution loop and withMessages() to PromptBuilder#273gziolo wants to merge 5 commits into
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## trunk #273 +/- ##
============================================
+ Coverage 86.49% 86.81% +0.31%
- Complexity 1327 1360 +33
============================================
Files 68 68
Lines 4295 4406 +111
============================================
+ Hits 3715 3825 +110
- Misses 580 581 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds first-class support in PromptBuilder for (1) appending messages to an existing conversation and (2) automatically resolving model-requested function calls via a bounded multi-round loop, returning loop metadata in the final result’s additional data.
Changes:
- Add
PromptBuilder::withMessages()to append fullMessageinstances (counterpart towithHistory()which prepends). - Introduce
FunctionCallResolverInterfaceand integrate an optional function call resolution loop into text generation, including token-usage aggregation and transcript exposure. - Add comprehensive unit tests plus supporting mocks/helpers for scripted multi-round text generation.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
src/Builders/PromptBuilder.php |
Adds withMessages(), resolver configuration, and the multi-round function call resolution loop with transcript + token aggregation. |
src/Tools/Contracts/FunctionCallResolverInterface.php |
Defines the resolver extension point (canResolve() + resolve()) used by the loop. |
tests/unit/Builders/PromptBuilderFunctionCallResolutionTest.php |
Covers loop behavior (happy path, transcript ordering, unresolved calls, max iterations, token aggregation, non-text capabilities) and withMessages() ordering. |
tests/traits/MockModelCreationTrait.php |
Adds a scripted text-generation mock model helper to simulate multi-round responses deterministically. |
tests/mocks/MockFunctionCallResolver.php |
Adds a mock resolver that records checked/resolved calls and can be customized via callbacks. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Fixes #272
What
This PR adds two things to
PromptBuilder, as proposed in #272:withMessages(Message ...$messages)to append full messages to the conversation. It is the counterpart towithHistory(), which prepends. This gives developers a public way to continue a conversation, for example to build a manual function call loop.An automatic function call resolution loop, enabled with a resolver:
The resolver interface
The extension point is a small interface with two methods:
The two steps are separate on purpose.
canResolve()must be free of side effects. It is called for every function call in a model response before any call is executed. This way, a round is either executed completely or handed back to the caller untouched.resolve()executes one call. Execution errors should be encoded in the returnedFunctionResponse, so the model can react to them.How the loop works
completed), when the round limit is reached (maxIterations), when the response looks incomplete (incompleteFunctionCalls, see below), or when the resolver cannot resolve a requested call (unresolvedFunctionCalls, so the caller can handle custom functions).toolCalls(for example a truncated response that hit the token limit) or when a function call has no name. Such calls are never executed. All three official providers map their finish reasons correctly for this check, including the Google provider, which detects tool calls itself because the Gemini API reportsSTOPfor them.BeforeGenerateResultEventandAfterGenerateResultEventfire for every round, so consumers can observe the loop or abort it.functionCallResolutionkey of the additional data of the final result.Why in the SDK
WordPress core is adding an ability resolution loop to the WP AI Client (Trac ticket 64865, wordpress-develop PR #12658). Because the SDK had no way to append messages or run the loop, that PR captures the messages and the resolved model from
BeforeGenerateResultEventand calls the model directly for later rounds. With this PR, the WordPress side shrinks to a thin resolver:canResolve()checks that the function name maps to a registered ability, andresolve()executes it. The workaround disappears. Any other consumer of the SDK gets the same loop for free.Note on scope
docs/REQUIREMENTS.mdsays the client must not include agents. My reading is that this loop is not an agent framework. It is a small, bounded loop on top of the existing function calling feature, and it stays fully under the control of the caller. Happy to adjust the docs wording as part of this PR if you agree, or to change the approach if you read the scope differently.Testing
The new tests in
tests/unit/Builders/PromptBuilderFunctionCallResolutionTest.phpcover the happy path, transcript ordering and roles, multiple calls in one response, the no-execution guarantee when a call cannot be resolved, the iteration limit, token usage aggregation, zero-round completion, invalid options, non-text capabilities, andwithMessages()ordering.composer lint(PHPCS and PHPStan) passes.🤖 Generated with Claude Code