Abilities: bridge wp_ability_execute_result onto agents_api_ability_executed#199
Conversation
…xecuted Adds WP_Agent_Ability_Lifecycle_Bridge, a passive observer that forwards WordPress 7.1's Abilities API execution lifecycle filters onto substrate-level actions. This first slice wires the wp_ability_execute_result filter: every WP_Ability::execute() call (whether or not it succeeded) emits an agents_api_ability_executed action with the same shape exposed by the underlying filter (ability name, result, normalized input, ability instance). The handler returns the result unchanged, so observers cannot accidentally transform an ability's output. This is the first of four lifecycle slices tracked in #94. Follow-ups will add hooks for wp_pre_execute_ability (approval boundary), wp_ability_normalize_input (principal injection), and wp_ability_permission_result (tool access policy). On WordPress < 7.1 the underlying filter never fires, so the registered handler stays idle — no version gate needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Should we use |
|
The tree uses both prefixes today, with a loose split:
The ability-executed event is closer to the first bucket — it's an observable stream that any consumer can subscribe to, not an entity lifecycle hanging off a substrate value object. `WP_Ability` lives in WP core, not in agents-api, so there's no `wp_agent_*` entity to anchor it to either. The natural sibling is `agents_api_loop_event` (also a multiplexable observability bus). So I'd keep `agents_api_ability_executed` as-is, and use the same prefix for the rest of the lifecycle bridge slices when they land (`agents_api_ability_invoked`, `agents_api_ability_completed`, etc.). Worth a follow-up to codify this in the README's "Public Surface" section since the convention is implicit today. |
|
Ok, merging |
Summary
Adds
WP_Agent_Ability_Lifecycle_Bridge, a passive observer that forwards WordPress 7.1's Abilities API execution lifecycle filters onto substrate-level actions.This first slice wires the
wp_ability_execute_resultfilter: every time an ability'sexecute_callbackruns (after permission and input checks pass),agents_api_ability_executedfires with the same shape as the underlying filter — ability name, result, normalized input, ability instance. The handler returns the result unchanged, so observers cannot accidentally transform an ability's output.Why
Tracked in #94. WP core PR WordPress/wordpress-develop#11731 landed in trunk on 2026-05-21 via changeset 62397, adding four execution lifecycle filters to
WP_Ability(wp_pre_execute_ability,wp_ability_normalize_input,wp_ability_permission_result,wp_ability_execute_result). The substrate's adoption plan in #94 mapped each filter to a substrate primitive; this PR is the smallest, lowest-risk first slice: observability via the post-execute filter.What this slice does and does not cover
wp_ability_execute_resultfires insidedo_execute(), so it sees the result only onceexecute_callbackwas reached. That meansagents_api_ability_executedfires for:execute_callbackreturned aWP_Errorexecute_callbackIt does not fire for:
wp_pre_execute_abilityshort-circuit (handled by a separate slice)wp_ability_normalize_inputreturningWP_Errorvalidate_input()failurepermission_callback/wp_ability_permission_resultdenialvalidate_output()failure after the filterThose failure modes get their own observer surfaces in follow-up slices. There is also no
duration_msavailable at this seam — the outerexecute()has not returned yet — so timing has to come from a later hook.Boundary
The bridge is a pure observer:
agents_api_ability_executed( \$ability_name, \$result, \$input, \$ability )— mirrors the filter's 4-arg shape verbatim.agents_api_loop_eventor the conversation loop. Ability execution can happen outside any agent flow (a REST call, a workflow step, a scheduled job) and the observer surface is consistent for all of them.On WordPress < 7.1 the underlying filter is never applied, so the registered handler stays idle. No version gate needed. (Note: capability detection by host version is unreliable because the Abilities API also ships standalone; future slices that need to detect host capability should reflect on
WP_Ability::execute()source rather than checking\$wp_version.)Changes
src/Abilities/class-wp-agent-ability-lifecycle-bridge.php— new class with one staticregister()and one observer.agents-api.php— require the new class; register the bridge oninitat priority 5.tests/ability-lifecycle-bridge-smoke.php— 12 assertions covering pass-through, failure result, multi-execution.tests/bootstrap-smoke.php— add'Abilities'to the source-directory whitelist.composer.json— include the new smoke incomposer test.Follow-ups (separate PRs in the #94 family)
wp_pre_execute_ability→ approval gate returning a pending-action envelope (resolves the sentinel-shape design question; @ibrahimhajjaj has offered to take this slice).wp_ability_normalize_input→ injectWP_Agent_Execution_Principal/WP_Agent_Caller_Contextinto ability input.wp_ability_permission_result→ route throughWP_Agent_Tool_Access_Policyand capability ceiling.Each of those reuses the same
WP_Agent_Ability_Lifecycle_Bridgeclass (extra static methods +add_filtercalls inregister()).Test plan
php tests/ability-lifecycle-bridge-smoke.php— 12 assertions, all pass.composer test— full suite green; bootstrap whitelist updated for the newsrc/Abilities/directory.Part of #94.