Skip to content

AI: Add automatic ability resolution loop to the WP AI Client - #12658

Draft
gziolo wants to merge 3 commits into
WordPress:trunkfrom
gziolo:add/64865-ability-resolution-loop
Draft

AI: Add automatic ability resolution loop to the WP AI Client#12658
gziolo wants to merge 3 commits into
WordPress:trunkfrom
gziolo:add/64865-ability-resolution-loop

Conversation

@gziolo

@gziolo gziolo commented Jul 23, 2026

Copy link
Copy Markdown
Member

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:

$text = wp_ai_client_prompt( 'What is this site about?' )
	->using_abilities( 'my-plugin/get-site-stats' )
	->using_ability_resolution( array( 'max_iterations' => 3 ) )
	->generate_text();

The method takes an options array so future options (for example a time limit or stop conditions) can be added without signature changes. max_iterations is the only option for now (default 5).

How it works

  • using_ability_resolution() enables the loop for generate_text_result() and generate_text(). Each round executes the ability calls requested by the model, appends the results to the conversation, and requests a follow-up response.
  • The loop ends when the model answers without ability calls, when it requests a function that is not a registered ability (the caller gets the round back to resolve custom functions), or when max_iterations is reached. The stop reason, the number of rounds, the resolved calls, and the full conversation are exposed under the ability_resolution key of the result's additional data.
  • Only abilities exposed to the model as ability function declarations, typically through using_abilities(), can be executed. Every call still runs the ability's permission check through WP_Ability::execute().
  • The wp_supports_ai() check and the wp_ai_client_prevent_prompt filter run before every round, so AI can be turned off mid-loop.
  • Token usage is summed across all rounds.
  • The AI client lifecycle events (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 PromptBuilder does 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 the BeforeGenerateResultEvent the 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 the WordPress/php-ai-client repository.

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

npm run test:php -- --filter="Tests_AI_Client_AbilityResolution|Tests_AI_Client_AbilityFunctionResolver"

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

Copilot AI review requested due to automatic review settings July 23, 2026 11:38
@github-actions

Copy link
Copy Markdown

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 props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props gziolo.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The 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

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

Copilot AI 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.

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 (default max_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 invoke using_function_declarations() directly, which replaces the model-callable functions but leaves $this->resolvable_abilities untouched from any prior using_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_abilities when using_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.

Comment thread src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php
Comment thread src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php
Copilot AI review requested due to automatic review settings July 23, 2026 11:55
@gziolo
gziolo force-pushed the add/64865-ability-resolution-loop branch from c10f3c8 to b15a0f5 Compare July 23, 2026 11:55

Copilot AI 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.

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.' );
			}

Comment thread src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php
Comment thread tests/phpunit/tests/ai-client/wpAiClientAbilityFunctionResolver.php Outdated
Comment thread tests/phpunit/tests/ai-client/wpAiClientAbilityResolution.php Outdated
Comment thread tests/phpunit/tests/ai-client/wpAiClientAbilityResolution.php Outdated
Copilot AI review requested due to automatic review settings July 23, 2026 12:01
@gziolo
gziolo force-pushed the add/64865-ability-resolution-loop branch from b15a0f5 to c5cafa3 Compare July 23, 2026 12:01
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>
@gziolo
gziolo force-pushed the add/64865-ability-resolution-loop branch from c5cafa3 to b629b4d Compare July 23, 2026 12:03

Copilot AI 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.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comment thread tests/phpunit/includes/wp-ai-client-mock-model-creation-trait.php
Copilot AI review requested due to automatic review settings July 23, 2026 12:04
@gziolo gziolo self-assigned this Jul 23, 2026

Copilot AI 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.

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 requires max_iterations to be a native int. 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 with absint() 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 $results is 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();

Copilot AI review requested due to automatic review settings July 23, 2026 12:44

Copilot AI 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.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment on lines +520 to +522
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 );
Copilot AI review requested due to automatic review settings July 23, 2026 13:18

Copilot AI 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.

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_result listener 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 ) {
Comment on lines +305 to +308
* 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.
@gziolo
gziolo marked this pull request as draft July 23, 2026 13:59
@JasonTheAdams

Copy link
Copy Markdown
Member

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?

@gziolo

gziolo commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

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:

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.

3 participants