Skip to content

feat(workflows): single step-type registry for handler + field validation - #575

Merged
chubes4 merged 1 commit into
mainfrom
feat/574-step-type-registry
Sep 27, 2026
Merged

chubes4 merged 1 commit into
mainfrom
feat/574-step-type-registry

Conversation

@chubes4

@chubes4 chubes4 commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

Fixes #574

API

WP_Agent_Workflow_Step_Type_Registry — one registry pairing a step type name with its runner handler and an optional field-validation contract:

register_workflow_step_type(
    'branch',
    array(
        'handler'  => callable,  // ( array $resolved_step, array $context ): array|WP_Error — required
        'validate' => callable,  // ( array $step, string $path ): list<{path,code,message}> — optional
        'required' => array( 'condition' ), // simple non-empty-string field check — optional
    )
);
  • register( string $type, array $args ): bool, get(), all(), types(), handlers(), validate_step( array $step, string $path ): array, reset() (test-only).
  • Built-ins (ability, agent, foreach, parallel) register lazily on first access — no init hook dependency, so it behaves identically in plain-PHP smoke tests and inside WordPress.
  • Duplicate register() for an already-registered type keeps the first registration and triggers _doing_it_wrong (guarded by function_exists()), returns false. No silent overwrite.
  • WP_Agent_Workflow_Spec_Validator::known_step_types() (new, public) is the registry's types() extended by the legacy filter — replaces the inline KNOWN_STEP_TYPES check.
  • WP_Agent_Workflow_Spec_Validator::validate_steps() bumped from private to public so the registry's built-in foreach/parallel validate callbacks can recurse into nested step lists through the same function, instead of a second copy living in the registry.

Back-compat plan (one release)

  • wp_agent_workflow_known_step_types and wp_agent_workflow_step_handlers are still applied on top of the registry's output, in the runner constructor, default_step_handlers(), agents_workflow_resolve_step_handlers(), and WP_Agent_Workflow_Spec_Validator::known_step_types(). All four call sites carry @deprecated 0.14.0 docblocks pointing at register_workflow_step_type().
  • WP_Agent_Workflow_Spec_Validator::KNOWN_STEP_TYPES const is kept (some consumer may still read it directly) but its docblock now says explicitly it is not the source of truth as of 0.14.0 — WP_Agent_Workflow_Step_Type_Registry::types() is.
  • A type registered only through the legacy filters still gets zero field validation, same gap the issue describes for unmigrated consumers — that's expected and covered by a smoke test.
  • No permanent alias; both filters are candidates for deletion in the next release per the issue.

Deleted duplicate maps

Three inline array( 'ability' => ..., 'agent' => ..., 'foreach' => ..., 'parallel' => ... ) maps deleted, each replaced by one WP_Agent_Workflow_Step_Type_Registry::handlers() call:

  1. WP_Agent_Workflow_Runner::__construct() (was ~line 76)
  2. WP_Agent_Workflow_Runner::default_step_handlers() (was ~line 1698)
  3. agents_workflow_resolve_step_handlers() in register-reconcile-workflow-branch.php (was ~line 958)

git grep "'parallel' => array( " now returns zero matches (the acceptance criteria's "one, not three" — the built-ins are registered as four separate self::register() calls in register_builtin_types(), not one combined array literal, so there's no map at all left to find, duplicated or otherwise).

Field validation moved into the registry

  • ability (required => ['ability']) and agent (required => ['agent','message']) use the registry's generic required-field check — their original messages ("{type} step is missing a non-empty \{field}`"`) match the generic format exactly, byte-for-byte.
  • foreach and parallel get dedicated validate callbacks in the registry (moved verbatim from the validator's old inline if ('foreach' === $step['type']) / validate_parallel_step()), recursing into nested step lists via the now-public WP_Agent_Workflow_Spec_Validator::validate_steps().

One correctness note found while moving this: is_callable() on a [ClassString, 'method'] pair triggers autoloading to verify the method exists. The registry's own built-ins point at WP_Agent_Workflow_Runner, which several existing workflow smoke tests deliberately don't load (e.g. workflow-spec-validator-smoke.php only loads the validator + spec). A strict is_callable() gate at registration silently rejected the built-ins in that context. Registration now does a structural shape check (Closure / non-empty string / [target, method] pair / invokable object) instead, deferring real callability to call_user_func() at dispatch time — same trust model the pre-registry code had (it stored these as plain array values without ever validating them).

Tests

New tests/workflow-step-type-registry-smoke.php (added to composer smoke), 27 assertions:

  • Custom type with required + validate: missing required field fails with the right path/code; the validate callback's own rule fails with the right path/code; a fully valid step passes; the registered handler actually runs via handlers() and returns its output.
  • Duplicate registration returns false, triggers _doing_it_wrong, and keeps the first handler (confirmed by invoking it).
  • Legacy two-filter registration (wp_agent_workflow_known_step_types + wp_agent_workflow_step_handlers) still passes structural validation and runs end-to-end through WP_Agent_Workflow_Runner.
  • Built-in ability/agent/foreach/parallel error codes, paths, and (for ability) the exact message are unchanged after the move, including a foreach-nested-step path check through the shared recursive validator.

Existing workflow-spec-validator-smoke.php and the 11 other smoke files that load the validator/runner/reconcile-branch file now also load class-wp-agent-workflow-step-type-registry.php; no assertions in any of them changed.

Results

composer smoke     — 27 test files, exit 0, no failures
composer phpstan   — No errors
composer phpstan-consumer — No errors

AI disclosure

AI assistance: Yes / Tool: OpenCode / Used for: implementation and tests

…validation

Introduces WP_Agent_Workflow_Step_Type_Registry, pairing each workflow
step type with its handler and an optional field validation contract
(a simple `required` field-name list, and/or a `validate` callback).
register_workflow_step_type() is the public helper.

Built-in types (ability, agent, foreach, parallel) register lazily on
first access; their field checks moved out of
WP_Agent_Workflow_Spec_Validator's inline per-type ifs and into the
registry's validate callbacks, byte-identical in error codes/paths/
messages.

Deletes the three duplicated default handler maps (runner constructor,
runner::default_step_handlers(), reconcile branch resolver) in favor
of one WP_Agent_Workflow_Step_Type_Registry::handlers() call each.
Duplicate register() calls keep the first registration and trigger
_doing_it_wrong.

Back-compat for one release: the legacy wp_agent_workflow_known_step_types
and wp_agent_workflow_step_handlers filters still apply on top of the
registry's output, marked @deprecated. KNOWN_STEP_TYPES stays for BC
but is documented as no longer the source of truth.

Fixes #574
@chubes4
chubes4 merged commit 1d1fc1a into main Sep 27, 2026
7 of 8 checks passed
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.

Workflow step types are defined in four places with no per-type validation — add a single step-type registry

1 participant