Skip to content

Channels: sender_id + tool context; workflows: ${vars}, foreach, dispatch-message#168

Merged
lezama merged 8 commits into
mainfrom
add/channels-context-and-workflow-primitives
May 13, 2026
Merged

Channels: sender_id + tool context; workflows: ${vars}, foreach, dispatch-message#168
lezama merged 8 commits into
mainfrom
add/channels-context-and-workflow-primitives

Conversation

@lezama
Copy link
Copy Markdown
Contributor

@lezama lezama commented May 13, 2026

Summary

Adds two layers of substrate primitives that consumers (WhatsApp adapters, multi-step workflows, message-broadcast use-cases) were repeatedly reaching past:

Channels + tool context

  • WP_Agent_Channel::extract_sender_id() extension hook so adapters can expose the user's external id (WhatsApp number, Slack user, etc.) to the runtime.
  • client_context.sender_id plumbed into the agents/chat schema so abilities can be authored against the canonical client context shape.
  • Tool execution merges runtime context into the args before validating required parameters. This lets a channel-supplied value (e.g. user_phone from sender_id) satisfy an ability's required arg without forcing every channel to duplicate that logic per turn.

Workflow primitives

  • \${vars.*} bindings alongside the existing \${steps.*.output.*} syntax, for hoisting workflow-level constants.
  • foreach step type so a workflow can fan a single step over an array (e.g. "for each pending prediction, score it").
  • Bug fix in the spec validator that was double-prefixing the `steps.` path component, masking real errors.

New ability + auto-sync

  • `agents/dispatch-message` ability with handler filter — lets a workflow send a message to a recipient without knowing the transport.
  • Workflow ↔ cron bridge auto-syncs cron triggers when a workflow CPT post is saved, so admins don't have to remember to schedule by hand.

Smoke tests for all of the above under `tests/`.

Test plan

  • Existing test suite passes.
  • Channel that overrides `extract_sender_id()` lands the value in `client_context.sender_id`.
  • Ability with required `user_phone` succeeds when only `sender_id` is supplied.
  • Workflow with `${vars.foo}` interpolates correctly.
  • `foreach` step iterates an array output from a prior step.
  • Saving a workflow post with a cron trigger registers the cron without manual intervention.
  • Calling `agents/dispatch-message` with a registered handler delivers; unregistered handler returns a clear error.

@chubes4
Copy link
Copy Markdown
Contributor

chubes4 commented May 13, 2026

Directionally this is aligned with what I would want Agents API to own, using Data Machine as the production adoption case.

Data Machine already has the big, messy version of this problem: durable flows, scheduled jobs, fan-out, tool execution, channel-ish chat surfaces, approval gates, and long-running automation. From that lens, these primitives are the right kind of substrate:

  • sender_id belongs in the canonical channel/client context contract. DM should not need per-adapter conventions for external human identity.
  • foreach + scoped ${vars.*} is the workflow primitive DM-style pipelines eventually need from the generic layer.
  • agents/dispatch-message is the right separation from agents/chat: chat is inbound agent execution, dispatch is outbound delivery through a transport.
  • Optional Action Scheduler integration is the right dependency posture. Agents API should use AS when present without making the whole substrate require it.

The part I would want tightened before adopting this for production Data Machine scheduling is the workflow lifecycle contract around cron auto-sync.

Right now the new sync hooks wp_agent_workflow_registered, which covers code/in-memory registration. That is useful, but DM’s production case is durable: flows/workflows are saved, updated, disabled, deleted, migrated, and sometimes repaired outside a normal plugin boot path. For that world, scheduler sync needs to follow durable lifecycle events, not only registration.

What I’d want from Agents API before DM could lean on this directly:

  • A generic durable workflow lifecycle surface: saved/updated/deleted/disabled or equivalent events around WP_Agent_Workflow_Store implementations.
  • Scheduler bridge behavior that can subscribe to those lifecycle events and keep AS in sync for create/update/delete, including unscheduling stale hooks.
  • Clear separation between “code registered this workflow for this request” and “this workflow is durably active and should have scheduled work.”
  • A way for stores/products to opt into the bridge without pretending their persisted workflows are code-registered at boot.

So I like the AS bridge pattern, and it does not conflict with Data Machine. I just wouldn’t want the auto-sync story to stop at wp_register_workflow(), because the production beast is persisted workflows with real lifecycle management.

One adjacent architectural note: the tool-context change is solving a real need, but I’d prefer required tool args to be satisfied by explicit context mappings/defaults rather than broad ambient context key matching. That keeps sensitive parameters auditable when a runtime like DM starts delegating more tool execution to Agents API.

lezama and others added 7 commits May 13, 2026 16:27
WordPress 7.0 has not been released. Drop to 6.7, which is the
oldest currently released line that ships the Abilities API hooks
agents-api relies on.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two coupled substrate changes that let WhatsApp/wacli channels deliver
caller identity to tool calls without asking the model to invent it.

WP_Agent_Channel gains an extract_sender_id() hook (default null) and
threads the value into WP_Agent_External_Message::sender_id. The
canonical agents/chat ability advertises the new client_context.sender_id
field in its input schema so external dispatchers can pass it.

WP_Agent_Tool_Execution_Core now merges the tool context into the
parameter set BEFORE validating required fields. Previously a required
parameter that the channel could satisfy via context was rejected at
the validation step. Tools whose schema marks user_phone as required
can now be invoked from a channel-driven turn as long as the runtime
context carries it.

Smoke tests updated to cover the new external-message field, the
schema property, and the context-fills-required path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Substrate features unlocked by building real consumers (penca-style
product) on top of the workflow engine.

Workflow bindings: \${vars.x.y} now resolves against scoped runtime
values populated inside foreach iterations. Lets a foreach body bind
ability arguments to the current iteration's item.

Workflow runner: new foreach step type. Iterates a bound array (e.g.
\${steps.find_matches.output.matches}) under a scoped name (`as`) and
runs an inner step list per item, with optional continue_on_error.

Workflow spec validator: nested step paths no longer prefix steps.
twice. Errors emerge as workflow.steps.0.steps.1 instead of
workflow.steps.0.steps.steps.1.

register-workflow-bridge-sync: action-scheduler bridge auto-syncs
workflow cron triggers when wp_register_workflow() fires. Drops the
need for consumers to wire schedule lifecycle themselves.

agents/dispatch-message ability: substrate-side ability that
workflow `dispatch` steps invoke to send outbound channel messages.
Validates channel/recipient/message, supports attachments, and runs
through a wp_agent_dispatch_message_handler filter so consumers
(openclaWP, custom plugins) provide the actual transport. Permission
defaults to manage_options with an agents_dispatch_message_permission
filter for narrower gating.

Smoke tests cover all primitives: vars resolution, foreach iteration,
spec validator path fix, dispatch-message wiring (handler, permission,
schema).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous commit dropped Requires at least from a fictional 7.0
to 6.7, but the Abilities API that this substrate registers against
via wp_register_ability() did not land in core until WordPress 6.9.
6.7 was still too low to guarantee the API surface is present.

Set Requires at least to 6.9, the actual minimum version that ships
wp_register_ability() in core. Add Tested up to: 6.9 to declare
forward-looking compatibility against the current stable line; bump
this with each new WP release after a smoke pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WordPress 7.0-RC3 is the in-development release the substrate is
written against (it ships the Abilities API surface this plugin
needs). The previous bumps in this branch were mistakes triggered by
running stable WP locally; the fix is to run a 7.0 dev build, not
weaken the substrate floor.

Tested up to: 7.0 aligns with Requires at least.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`wp_agent_workflow_registered` only covers code-defined workflows added
to the in-memory registry during plugin boot. Durable workflows backed
by a WP_Agent_Workflow_Store need their own event surface so consumers
can react to saved / deleted / disabled / enabled state changes outside
the request boot path.

- Add WP_Agent_Workflow_Lifecycle helper that fires canonical hooks for
  saved, deleted, disabled, and enabled events.
- Document the lifecycle contract on the WP_Agent_Workflow_Store
  interface (implementations call the helper after each store op).
- Add WP_Agent_Workflow_Action_Scheduler_Bridge::sync() that unschedules
  every existing AS action for the workflow id before re-registering
  cron triggers, so updates that drop a cron trigger don't leak stale
  schedules.
- Wire the bridge-sync subscriber to react to saved/deleted/disabled/
  enabled events, distinct from the existing registered hook.
- Add tests/workflow-lifecycle-smoke.php.

Addresses review feedback on PR #168: durable workflows need lifecycle
events independent of in-memory registration so AS stays in sync with
create/update/delete and unschedules stale hooks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Broad ambient context-key matching let any host-supplied context value
silently satisfy a tool's required parameter just by sharing a name.
That keeps sensitive parameters un-auditable when a runtime delegates
tool execution to substrate code.

- WP_Agent_Tool_Parameters::buildParameters now only pulls context
  values for slots explicitly declared in the tool's
  `client_context_bindings` field. Caller-supplied parameters still
  win over bindings.
- Declarations accept either an associative form (parameter_name =>
  context_key, allows renaming) or a flat list shorthand for same-name
  bindings.
- Empty / null context values are skipped so they can't silently
  satisfy a required-parameter check.
- Adapter in tool-runtime-smoke now reads ambient context (`request_id`,
  `agent_id`) from the executor's `$context` arg rather than expecting
  them to be merged into `$tool_call['parameters']`. Adds coverage for
  rename bindings and confirms undeclared context keys cannot satisfy a
  required parameter.

Addresses review feedback on PR #168: keep context-to-parameter
injection explicit so authors of sensitive abilities know exactly which
context keys can reach a required argument.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lezama lezama force-pushed the add/channels-context-and-workflow-primitives branch from 55a5478 to 5dc4195 Compare May 13, 2026 19:35
@lezama
Copy link
Copy Markdown
Contributor Author

lezama commented May 13, 2026

Thanks for the close read. Pushed two follow-ups:

f32d2c8 introduces WP_Agent_Workflow_Lifecycle (saved / deleted / disabled / enabled) as the durable event surface, separate from wp_agent_workflow_registered. WP_Agent_Workflow_Store now documents the contract — stores call into the helper after each op. The AS bridge gained a sync() that unschedules every existing action for the workflow id before re-registering, so updates that drop a cron trigger don't leak stale schedules. Bridge-sync subscribes to all four lifecycle hooks; _registered stays the in-memory-only signal.

5dc4195 drops the broad ambient context-key matching for required tool args. Tools now opt in via client_context_bindings (associative param => context_key for rename, flat list for same-name). Undeclared context keys cannot satisfy a required parameter, so sensitive abilities stay auditable end-to-end.

Both covered by smoke tests (workflow-lifecycle-smoke.php + extended tool-runtime-smoke.php).

@chubes4
Copy link
Copy Markdown
Contributor

chubes4 commented May 13, 2026

Architecture concerns from the Data Machine adoption lens are resolved for me. The durable lifecycle hooks + sync() bridge are the right shape for persisted workflow stores, and client_context_bindings fixes the ambient-context issue cleanly.

Only thing from my side: fix the current lint failure, then let 'er rip.

Homeboy/WPCS reported double-arrow and equals-sign alignment violations
introduced by the new `agents/dispatch-message` schema (where
`conversation_id` is the longest key in the `properties` map) and the
`foreach` step nested-output bookkeeping in the workflow runner.

- `register-agents-dispatch-message-ability.php`: pad the six shorter
  property keys by one space so every `=>` lines up with the
  `conversation_id` row.
- `class-wp-agent-workflow-runner.php`: pad the `$step_outputs[...]`
  assignment by two spaces so its `=` aligns with the wider
  `$iteration_context['steps'][...]` assignment below it.

Pure whitespace; smoke suite still passes (all 1100+ assertions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lezama lezama merged commit 46e40fb into main May 13, 2026
2 checks passed
@lezama lezama deleted the add/channels-context-and-workflow-primitives branch May 13, 2026 20:09
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.

2 participants