AI: Add automatic ability resolution loop to the WP AI Client - #12658
AI: Add automatic ability resolution loop to the WP AI Client#12658gziolo wants to merge 3 commits into
Conversation
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Test using WordPress PlaygroundThe changes in this pull request can previewed and tested using a WordPress Playground instance. WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser. Some things to be aware of
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
There was a problem hiding this comment.
Pull request overview
Adds an opt-in automatic “ability resolution” loop to the WordPress AI Client prompt builder, allowing generate_text_result() / generate_text() to automatically execute model-requested ability calls and continue the conversation until completion or a stopping condition.
Changes:
- Introduces
using_ability_resolution()with configurable loop options (defaultmax_iterations), and captures/extends the message transcript across rounds while aggregating token usage. - Adds a pre-resolve filter to short-circuit individual ability calls and an action for logging/auditing resolved calls.
- Adds PHPUnit coverage for loop behavior (transcripts/roles, stopping reasons, token aggregation, prevent filter, lifecycle events) and extends test utilities with a scripted model.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php |
Implements the opt-in ability-resolution loop, options handling, transcript capture/continuation, and token aggregation. |
src/wp-includes/ai-client/class-wp-ai-client-ability-function-resolver.php |
Adds per-call pre-resolve short-circuiting and a resolved-call action hook for auditing. |
tests/phpunit/includes/wp-ai-client-mock-model-creation-trait.php |
Adds a scripted text model helper to support multi-round loop tests. |
tests/phpunit/tests/ai-client/wpAiClientAbilityResolution.php |
New test suite covering the resolution loop end-to-end and its metadata/edge cases. |
tests/phpunit/tests/ai-client/wpAiClientAbilityFunctionResolver.php |
Adds tests for the new pre-resolve filter and resolved-call action behavior. |
Comments suppressed due to low confidence (1)
src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php:441
call_builder()allows callers to invokeusing_function_declarations()directly, which replaces the model-callable functions but leaves$this->resolvable_abilitiesuntouched from any priorusing_abilities()call. With ability resolution enabled, that stale allowlist can permit executing abilities that are no longer declared (e.g., via prompt-injected function names). Clearing$this->resolvable_abilitieswhenusing_function_declarations()is called keeps the execution allowlist aligned with what was actually exposed to the model.
// Check if the prompt should be prevented for is_supported* and generate_*/convert_text_to_speech* methods.
if ( self::is_support_check_method( $name ) || self::is_generating_method( $name ) ) {
$prevented = $this->get_prompt_prevented_error();
if ( null !== $prevented ) {
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
c10f3c8 to
b15a0f5
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
tests/phpunit/tests/ai-client/wpAiClientAbilityFunctionResolver.php:925
- This filter callback declares 0 parameters, but
add_filter()will pass the filtered value as the first argument by default. To avoid argument count errors when the filter executes, accept the first argument (even if unused).
static function () {
return new WP_Error( 'vetoed', 'This call is not allowed.' );
}
b15a0f5 to
c5cafa3
Compare
Introduces using_ability_resolution() on WP_AI_Client_Prompt_Builder. When enabled, the text generation methods run a resolution loop: each round executes the ability function calls requested by the model, appends the results to the conversation, and requests a follow-up response, until the model produces a final answer, requests an unknown function, or the maximum number of rounds is reached. Also adds the wp_ai_client_ability_resolution_defaults and wp_ai_client_pre_resolve_ability_call filters and the wp_ai_client_ability_call_resolved action. See #64865. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
c5cafa3 to
b629b4d
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php:343
using_ability_resolution()currently requiresmax_iterationsto be a nativeint. That means common values like'2'(numeric strings) coming from filters/options will be treated as invalid and silently revert to 5, potentially increasing loop iterations unexpectedly. Consider normalizing withabsint()for both filtered defaults and caller options, and only warning when the normalized value is < 1.
// Guard against invalid filtered defaults.
if ( ! is_int( $defaults['max_iterations'] ) || $defaults['max_iterations'] < 1 ) {
$defaults['max_iterations'] = 5;
}
tests/phpunit/includes/wp-ai-client-mock-model-creation-trait.php:243
create_scripted_text_generation_model()assumes$resultsis non-empty; if an empty array is passed,generateTextResult()will hit an undefined offset ($this->results[0]), making failures harder to diagnose. Add an explicit guard early with a clear exception message.
protected function create_scripted_text_generation_model(
array $results,
array &$captured_prompts,
?ModelMetadata $metadata = null
): ModelInterface {
$metadata = $metadata ?? $this->create_test_text_model_metadata();
| add_action( 'wp_ai_client_before_generate_result', $capture ); | ||
| $result = $this->call_builder( 'generate_text_result', array() ); | ||
| remove_action( 'wp_ai_client_before_generate_result', $capture ); |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php:522
- The capture callback is added at the default priority (10). If any existing
wp_ai_client_before_generate_resultlistener with an earlier priority triggers another AI request, this closure can capture the nested request’s event instead of the current one. Consider registering this capture handler at an extremely early priority to make the capture deterministic.
add_action( 'wp_ai_client_before_generate_result', $capture );
$result = $this->call_builder( 'generate_text_result', array() );
remove_action( 'wp_ai_client_before_generate_result', $capture );
| return $result; | ||
| } | ||
|
|
||
| if ( null === $captured || ! $captured->getModel() instanceof TextGenerationModelInterface ) { |
| * Resolution follows the first response candidate and supports the | ||
| * generate_text_result() and generate_text() methods. Details about the loop | ||
| * are exposed under the `ability_resolution` key of the additional data of | ||
| * the final result. |
|
This is cool, @gziolo! Felix and I chatted about eventually adding something like this. I wonder if it would be better to introduce agentic looping on function declarations in the PHP AI Client. We could add an event, overloadable function, or some such thing to make it possible for WP to handle Abilities. What do you think? |
|
Thank you for the feedback, @JasonTheAdams. I filed a new issue in the PHP AI client SDK: I also drafted a PR with the initial proposal to keep the discussion going: |
Trac ticket: https://core.trac.wordpress.org/ticket/64865
What
Adds an automatic ability resolution loop to the WP AI Client, as proposed in the ticket. Today, when a model responds with an ability function call, developers must detect it, execute the ability, feed the result back, and re-prompt the model by hand. This PR makes that loop opt-in with one fluent method:
The method takes an options array so future options (for example a time limit or stop conditions) can be added without signature changes.
max_iterationsis the only option for now (default 5).How it works
using_ability_resolution()enables the loop forgenerate_text_result()andgenerate_text(). Each round executes the ability calls requested by the model, appends the results to the conversation, and requests a follow-up response.max_iterationsis reached. The stop reason, the number of rounds, the resolved calls, and the full conversation are exposed under theability_resolutionkey of the result's additional data.using_abilities(), can be executed. Every call still runs the ability's permission check throughWP_Ability::execute().wp_supports_ai()check and thewp_ai_client_prevent_promptfilter run before every round, so AI can be turned off mid-loop.wp_ai_client_before_generate_result/wp_ai_client_after_generate_result) fire for every round.Note: a PHP AI Client change could simplify the internals
The PHP AI Client
PromptBuilderdoes not expose its message list, and there is no method to append full messages to it (withHistory()prepends). Because of that, the first request here captures the sent messages and the resolved model from theBeforeGenerateResultEventthe builder dispatches, and later rounds call the captured model directly.This only uses public API and works today without upstream changes. Still, a small addition to the PHP AI Client (for example a
withMessages()append method) would let the loop run on the builder itself, remove the capture step, and give plugin developers a public way to build manual loops and multi-request conversations (see the report in ticket comment 2). If we agree on the direction, I will file an issue in theWordPress/php-ai-clientrepository.Related known issue: the OpenAI provider plugin rejects Model-role messages sent back as input (
Invalid value: 'output_text', also from ticket comment 2). That affects any multi-turn continuation, including this loop, and needs its own fix in the provider plugin.Testing
The new tests cover the happy path, transcript ordering and roles, multiple calls in one response, error and not-allowed responses fed back to the model, the iteration limit, unknown function calls, the prevent filter mid-loop, token usage aggregation, and lifecycle events.
🤖 Generated with Claude Code