Skip to content

AI Agents framework: agents as WordPress users, abilities runner, chat with persisted conversations, drag & drop and Send to triggers - #428

Merged
epeicher merged 22 commits into
trunkfrom
add/agents-phase-a
Jul 31, 2026
Merged

AI Agents framework: agents as WordPress users, abilities runner, chat with persisted conversations, drag & drop and Send to triggers#428
epeicher merged 22 commits into
trunkfrom
add/agents-phase-a

Conversation

@epeicher

@epeicher epeicher commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

What it does

Ships the AI Agents framework: durable workers that live on the site as real WordPress users, take orders by chat, drag & drop, or the "Send to" right-click menu, and act through the WordPress Abilities API under their own role and capabilities. The remaining trigger intakes (hook, REST endpoint, agent-to-agent) are follow-up work; their kinds are already declared and marked "coming soon" in the UI, so their configuration stores today and the intakes plug in without a storage migration.

Behind the new agents extended option (OS Settings → Features → Extended options, admin-only, default off). With the flag on, a new Agents section appears in My WordPress: create an agent (name, role, description, system prompt), tick the abilities it may use in the Tools pane, configure triggers, and talk to it in the Agent chat window. Every tool call runs as the agent's own user and lands in the standard audit trail (revisions, comments, _edit_lock) attributed to the agent.

Rationale

This is the buildable slice of the direction #240 mocked, with one deliberate storage change: no wp_guideline CPT. An agent is split across exactly two layers:

  • Identity: a synthetic wp_users row. Real role, real capability checks, real attribution. Every login path is blocked (authenticate filter, password resets, application passwords), the email is a never-delivered synthetic address, and the wp-admin Users list labels the row "Agent".
  • Definition: user meta on that row (_desktop_mode_agent_*): description, instructions, ability allowlist, triggers, model override, rate limit.

Dropping the guideline CPT removes the Gutenberg Guidelines-experiment dependency entirely (no soft-gate, no 412 paths). The accepted trade-off is losing free revisions on prompt edits; instead, every mutation fires desktop_mode_agent_{created,updated,deleted} with before/after values per changed field, so logging plugins get a complete audit trail.

Implementation

Runner (includes/agents/runner.php): desktop_mode_agent_invoke() generates through the same Core AI Client adapter the Copilot uses (desktop_mode_ai_client_generate() over wp_ai_client_prompt()), loops function calls to a hard 8-turn cap, and dispatches each call via WP_Ability::execute() so the ability's own permission_callback and schemas gate it. The whole loop runs with wp_set_current_user() switched to the agent, restored in finally:

$previous_user_id = get_current_user_id();
wp_set_current_user( $user->ID );
try {
    $result = desktop_mode_agent_runner_loop( ... );
} finally {
    wp_set_current_user( $previous_user_id );
}

Tool schemas advertised to the model are projected through the Copilot's desktop_mode_ai_normalize_tool_schema() (providers 400 the whole request over one tool with a top-level oneOf/anyOf/allOf). Per-agent hourly rate limits ride a transient counter (default 60/hour, filterable). A desktop_mode_agent_runner_generate pre-filter short-circuits the AI Client, which is how PHPUnit exercises the full loop network-free.

WordPress 7.0 AI Client: all generation runs on the AI Client introduced in WordPress 7.0 — every agent turn builds a wp_ai_client_prompt( $messages ) fluent chain (using_system_instruction() for the agent's prompt, using_function_declarations() to advertise abilities as tools via the SDK's FunctionDeclaration/FunctionCall/FunctionResponse DTOs, generate_result() to run it). Provider, model, and API keys are delegated entirely to the Core Connectors API (Settings → Connectors plus the official Anthropic/Google/OpenAI provider plugins); Desktop Mode pins none of them. The UI's availability probe is the client's own feature detection (is_supported_for_text_generation()). The JS-side wp-ai-client package is deliberately not used — generation stays server-side inside /invoke, matching Core's guidance for distributed plugins. Two places we go beyond the stock client: the function-calling tool loop, and thought-part handling (provider plugins don't round-trip Gemini/Anthropic thinking signatures, so desktop_mode_ai_strip_thought_parts() plus transcript replay keeps multi-turn tool loops working).

Tools: the picker is a view over wp_get_abilities() with honest read-only vs mutating badges from meta.annotations.readonly. Unlike the Copilot (read-only only), agents may be granted mutating abilities; the compensating controls are the explicit allowlist set by an edit_users human plus the agent's role. Five new abilities ship: desktop-mode/get-post and desktop-mode/get-media (read-only), plus the mutating trio desktop-mode/update-post, desktop-mode/update-media (alt text / title / caption / description), and desktop-mode/create-post (status hard-forced to draft — it can never publish); mutating abilities are reachable only through an agent allowlist.

REST (/desktop-mode/v1/agents): CRUD + /invoke + abilities/trigger-kinds/hooks/roles catalogues. Reads and invokes default to edit_posts, writes to edit_users (all filterable); role assignment is constrained to a whitelist intersected with the acting user's get_editable_roles().

Client: an agent entity kind registered through the existing registerEntityKind() seam in the My WordPress bundle (list, Define/Tools/Triggers panes, create flow, live provider probe against /ai/status), plus a new lazy agent-run-window bundle for the chat window, fed through the cross-bundle desktop-mode/agents-chat shared store.

Triggers: chat, drag & drop, and Send to are wired. A drop is a chat whose message carries the dropped entity: agents accept entity drops on their rows in the Agents section, on their user tiles on the wallpaper (via the files layer's tile-payload-handler seam, gated synchronously by agentDragKinds inlined into the user-file payload from the agent's drag trigger), and in the open chat window. Send-to rides the site folder's tile-context-menu filter seam: agents whose triggers include a send-to row appear as "Send to " entries in the tile context menus (posts, pages, media, users), gated by the trigger's entityKinds; the users grid was refactored onto the same filter seam the posts and media grids already used. The remaining kinds (hook, endpoint, agent) carry wired: false in the trigger-kind catalogue and render as disabled "coming soon" options in the Triggers pane; their intakes land in later phases. All of them collapse to the same desktop_mode_agent_invoke() engine, and desktop_mode_agent_completed is the chaining seam.

Chat window UX: WhatsApp-style avatars (agent on the left, the viewer's own avatar on the right, fed from the window config), agent answers rendered as markdown through a new shared src/markdown.ts (extracted from the AI assistant; supports headings, lists, bold/italic/code, safe links, thematic breaks, and applies inline tokens per line so stray asterisks cannot pair across lines), a readable in-flight bubble instead of a tiny chip, and a past-conversations sidebar ("+ New chat" on top, then the saved conversations with the agent's avatar, derived title, active highlight, and a hover delete with confirm). The Agents window detail head also gains Open profile (opens the agent's user-edit window) and Send to Desktop (creates a wallpaper tile on the first free grid cell), and agent rows in the list are draggable straight onto the desktop, producing the same shortcut tile.

Default agents (includes/agents/defaults.php): a fresh site gets a ready-to-use roster the first time an edit_users admin loads wp-admin with the flag on — tl;dr, Comment Concierge, Localizer (author role, drafts only), SEO Medic, and Alt Text Librarian, each with a full system prompt, ability allowlist, and chat + send-to + drag triggers. Seeding runs once per site and ONLY when no agents exist at all; an install that already built its own roster gets the seeded flag without any rows. Allowlisted abilities that aren't registered (the ai/* family from the AI experiments plugin) are skipped at tool-build time, so they light up automatically when the provider plugin lands.

Call-to-action buttons: agents that need the user's confirmation return renderable buttons instead of asking for a typed reply. The runner constrains every final answer to a { text, call_to_actions } JSON schema through the AI Client's structured output (as_json_response()), and a system-prompt appendix teaches the convention, so existing agents pick it up without prompt edits. Each action carries a label, a style (primary/secondary/dangerwpd-button variants), and a reply — the literal message posted back as a visible user turn when the button is pressed, so the stored history shows exactly what was approved. Parsing is lenient (non-JSON answers pass through verbatim with no actions), sanitization caps 4 actions / 40-char labels / 500-char replies, only the latest message's buttons are live, and spent buttons persist disabled with the conversation.

Persisted conversations (includes/agents/conversations.php): each conversation is one post of the private desktop_mode_chat post type — post_author is the human, messages as JSON in post_content, agent id in meta, title derived from the first user message. The posts table was chosen over user meta deliberately (WordPress loads all of a user's meta into cache on any meta read, so fat transcripts would tax every request). REST CRUD lives under /agents/conversations and is strictly owner-only — foreign rows read as 404 so ids cannot be probed, and not even administrators can read another user's chats through this API. The shared dispatcher auto-saves after every completed exchange (chat, drag, and send-to all persist; failures are swallowed so persistence can never break the conversation). Caps: 100 conversations per user (desktop_mode_agent_conversation_cap filter) with LRU pruning, 200 messages per conversation, tool-call outputs dropped on store. Each /invoke still replays the visible transcript via the history param, now capped at 50 turns (desktop_mode_agent_history_turn_cap filter).

Testing instructions

npm run test:php -- --filter='Tests_DesktopMode_Agents'   # 64 tests
npm run test:js                                            # includes the agents, send-to, chat window, and markdown suites

Manual, on a WP 7.0+ site with an AI connector configured:

  1. OS Settings → Features → Extended options → tick Enable AI agents, reload. On a site with no agents, the five default agents appear in the Agents section on the next wp-admin load.
  2. My WordPress → Agents → create an agent (role author, prompt like "You audit posts"). Tick desktop-mode/get-post + desktop-mode/update-post in Tools.
  3. Press Chat, ask "Fetch post 1 and tighten its title". Expect an answer with an expandable tool-call trace, and the post's revision attributed to the agent user.
  4. Verify the blocks: the agent appears in wp-admin Users with the bot avatar and "Agent" type, logging in as it fails, and no application passwords can be created for it.
  5. Without a connector, the section shows a warning notice pointing at Settings → Connectors and Chat is disabled; definitions remain editable.

Drag & drop (verified live in the browser against a real connector):

  1. In the agent's Triggers pane, add the Drag & drop trigger and tick the entity kinds it should accept (nothing ticked = every kind). Without this trigger every drop shows the no-drop cursor.
  2. Drag the agent's user tile from My WordPress → Users onto the wallpaper — the resulting user tile is a live drop target.
  3. Drag a media tile from My WordPress → Media onto that desktop tile, onto the agent's row in the Agents section, or into the open Agent chat window. Expect the Send to agent chip while hovering, then the chat window opening with the composed drop message and the run streaming into the transcript (source: 'drag' reaches desktop_mode_agent_completed).
  4. Dropping a kind outside the trigger's filter (e.g. a post onto a media-only agent) and dropping the agent's own user tile onto itself are both rejected.

Send to and window buttons (verified live in the browser against a real connector):

  1. In the agent's Triggers pane, add the Send to (right-click menu) trigger and tick the entity kinds. Right-click a matching tile in the site folder (posts, pages, media, users) and pick Send to : the chat window opens with the composed message and the run lands in the transcript (source: 'send-to').
  2. In the Agents window, Open profile opens the agent's user-edit window and Send to Desktop places the agent's tile on the first free desktop grid cell (an agent already on the desktop gets its existing tile moved there instead of duplicated).
  3. The trigger-kind picker renders WordPress hook, REST endpoint, and Agent-to-agent as disabled "coming soon" entries.

Persisted conversations (verified live in the browser against a real connector):

  1. Chat with an agent, then press + New chat and chat again: the sidebar lists both conversations, clicking the older one reloads its transcript (and re-targets the chat to its agent), and the list survives a full page reload.
  2. Hovering a sidebar row reveals a delete control; deleting the open conversation also clears the transcript. Conversations are owner-only — another user's list is empty and their ids read as 404.

Call-to-action buttons (verified live in the browser against a real connector):

  1. Ask an agent to propose a change and request approval (e.g. "Propose a TL;DR for post 860 and ask for my approval before applying it"): the answer arrives with Approve/Cancel buttons instead of a typed-confirmation request. Pressing Approve posts the action's reply as a visible user message, the agent applies the change through its tools, and the spent buttons stay visible but disabled.
Open WordPress Playground Preview

epeicher added 11 commits July 27, 2026 19:43
Agents are durable workers that live on the site as real WordPress
users and act through the Abilities API under their own role. An agent
is a login-blocked wp_users row plus a _desktop_mode_agent_* user-meta
family holding the whole definition: description, instructions
(system prompt), ability allowlist, triggers, model override, rate
limit. No wp_guideline dependency; the audit trail for definition
changes is the desktop_mode_agent_{created,updated,deleted} actions,
each carrying before/after values.

Behind the new 'agents' extended option (default off, games-style
module gating). Phase A ships:

- includes/agents/: store (meta CRUD + orchestrators), identity
  (login/password-reset/app-password blocks, bot avatar, Users list
  column), abilities bridge (desktop-mode/get-post + update-post,
  picker catalogue with readonly badges), runner on the Core AI
  Client (8-turn cap, per-agent hourly rate limit, identity switch in
  try/finally, provider-safe tool-schema normalization shared with
  the Copilot), REST CRUD + /invoke + catalogues, privacy
  exporter/eraser, Agent chat native window, My WordPress entity.
- Client: 'agent' entity-kind renderer in the My WordPress bundle
  (list, Define/Tools/Triggers panes, create flow, live AI-provider
  probe) and a new lazy agent-run-window bundle fed through the
  cross-bundle desktop-mode/agents-chat shared store.
- 45 PHPUnit + 20 vitest tests; docs (hooks-reference,
  javascript-reference, api-index, architecture, rest README,
  examples/agents.md) and the implementation plan under docs/plans/.

Chat is the only wired trigger; send-to/drag, hook, endpoint, and
agent-to-agent intakes are declared in the trigger-kind catalogue and
land in later phases.
Two Gemini-surfaced fixes in the agent invocation path:

- Strip the WordPress-only arg-schema keys (sanitize_callback,
  validate_callback, arg_options) from tool schemas at every depth, in
  the shared desktop_mode_ai_normalize_tool_schema() so the Copilot
  benefits too. Strict providers reject any unknown field and 400 the
  whole request over one property. The walk is structure-aware:
  property NAMES are never stripped, only schema-level keys; covers
  properties, patternProperties, single and tuple items, array-shaped
  additionalProperties, and nested combinators.

- Stop replaying assistant functionCall turns to the provider. Each
  generate turn now sends one user message: the original request plus
  a text transcript of executed tool calls and their results.
  Replaying functionCall parts requires provider-specific signatures
  (Gemini thought_signature, Anthropic thinking signatures) that the
  current provider plugins do not round-trip, and one missing
  signature 400s the request. Text transcripts carry the same
  information with no signature or pairing constraints on any
  provider.
Agents (and the Copilot, via the readonly annotation) can now read a
media library item by id: file URL, mime type, dimensions, alt text,
caption, and the post it is attached to. Closes the gap where no
ability on a stock site could read attachments, so image-referencing
prompts dead-ended.

Permission gates on upload_files (author+), deliberately not on
read_post: for inherit-status attachments that check defers to the
parent (and effectively requires edit rights when unattached), which
wrongly blocks read-only access to media whose file URL is public on
a standard site anyway.
…ity)

New standalone extension plugin under extensions/, riding the agents
PR as the first real consumer of the framework: an ability that
removes the background from a media library image and sideloads the
result as a NEW png attachment (original untouched), authored by the
calling user — for agents, the agent's own account, completing the
attribution story end to end.

Mutating ability (no readonly annotation): invisible to the Copilot,
reachable only through an agent's explicit allowlist. Pluggable
backends behind desktop_mode_remove_background_backends: remove.bg
(default, key required), a self-hosted rembg server, and an
experimental WordPress AI Client generative-editing backend that
reuses the site's configured connectors. Settings live natively on
Settings -> Media. A desktop_mode_remove_background_pre short-circuit
filter keeps PHPUnit network-free; seven tests cover registration,
the execute lifecycle, permissions, and runner dispatch with
agent attribution.
The extension's only user-facing surface is the ability itself — no
Settings -> Media section. Configuration is code/CLI-level, resolved
option -> constants (DESKTOP_MODE_REMOVE_BG_{BACKEND,API_KEY,ENDPOINT})
-> desktop_mode_remove_background_settings filter, with an unknown
backend slug falling back to the default. README documents all three
paths; error messages point at them instead of the removed screen.
The ai backend rides the site's existing Connectors credentials, so a
stock install needs no extension-specific key at all — remove.bg and
rembg become the opt-in paths for mask-based quality. The AI Client
resolves the model from the prompt's modalities (an input image
requires an image-input-capable model), so providers that only do
text-to-image are never silently picked for an edit.
wp_ai_client_prompt() returns WordPress's snake_case wrapper
(WP_AI_Client_Prompt_Builder), not the SDK PromptBuilder: generating
methods are generate_* and failures come back as WP_Error instead of
exceptions. The camelCase generateImageResult() call fell through the
wrapper's fluent path and returned the builder itself, which the
backend then reported as 'no readable image data' regardless of what
the provider said. Now calls generate_image_result(), propagates
WP_Error verbatim (so provider errors like quota exhaustion surface
readably in the agent's tool trace), and keeps the try/catch for
result-shape surprises. Regression test drives the real builder path
with no connector configured.
# Conflicts:
#	includes/extended-options.php
Three related symptoms, one root cause for two of them: the agent
avatar shipped as a data: URI, and 'data' is not in
wp_allowed_protocols(), so every consumer that runs avatars through
esc_url() (wp-admin's get_avatar(), the desktop user-tile icon, the
My WordPress entity tile) stripped it to an empty string — broken
avatar in the Users screen, icon-less desktop tile when dragging an
agent user to the wallpaper, and the letter-badge fallback on the
Agents folder tile. The bot avatar now ships as a static SVG
(assets/images/agent-avatar.svg, light disc + dark glyph so it reads
on light and dark surfaces) and every PHP surface uses its URL.

The Agents folder also showed 'Agents · 0': the root grid derives
folder counts from the X-WP-Total collection header, which the agents
list route never sent. It now emits X-WP-Total / X-WP-TotalPages.
The PR #240 North Star, wired through the existing drag machinery. A
drop is a chat whose message carries the dropped entity: the shared
dispatch engine (src/agents-dispatch.ts) normalizes the drag payload
('shortcut' from My WordPress tiles / wpd-tile drag-out,
'desktop-file' from wallpaper tiles) into { kind, id, title },
composes the invocation message, seeds the cross-bundle chat store,
surfaces the Agent chat window, and runs /invoke with source=drag so
the conversation shows the run live.

Three intake surfaces:
- Agent rows in the My WordPress Agents section (drop targets
  re-registered per paint, pruned per agent, torn down with the
  mount).
- Agent user tiles on the wallpaper, opted in through the files
  layer's tile-payload-handler seam. Gating is payload-driven: the
  user-file payload now inlines isAgent + agentDragKinds (the drag
  trigger's entityKinds; null = no drag trigger, [] = all kinds), so
  accept() stays synchronous with no REST roundtrip.
- The open Agent chat window, which accepts drops for the active
  agent without trigger gating — dropping into an open conversation
  is explicit intent, like typing.

The invoke route gains a source param (chat|drag|send-to) that lands
in the completed action's context for audit and future chaining.
Self-drops (an agent's own user tile) are always rejected.
Two live-verified fixes from browser-testing the drag intake on a
real site:

- The wallpaper tile drop handler built its invoke URL from the files
  layer's injected baseUrl, which already ends in
  desktop-mode/v1/files — the request went to
  .../files/desktop-mode/v1/agents/{id}/invoke and 404ed
  (rest_no_route). The handler now reads the shell config's restUrl
  (rest_url()) directly; a regression test pins that the URL never
  contains /files/.

- My WordPress root entity tiles ran every icon through the class
  sanitizer, so a URL icon (the Agents entity's bot SVG) was mangled
  into an invalid dashicon class and fell back to the letter badge.
  URL- and data-URI-shaped icons now pass through untouched, per
  wpd-tile's documented contract.

Verified end to end in the browser: dragging a media tile from
My WordPress onto the agent's desktop tile shows the Send-to-agent
chip, opens the chat window, and completes the invocation.
@epeicher
epeicher marked this pull request as ready for review July 29, 2026 16:10
@epeicher epeicher changed the title AI Agents framework, Phase A: synthetic users + user-meta definitions, abilities runner, chat AI Agents framework: user-backed agents, abilities runner, chat + drag-and-drop triggers, Remove Background extension Jul 29, 2026
epeicher added 8 commits July 29, 2026 18:20
Deleting the last agent left the dead agent's tabs + Define form
painted above the fresh 'No agents yet' empty state. The renderer
bug is general, not agents-specific: disposing a template instance
removed only the nodes cloned at mount (state.nodes), but a child
part whose anchor sits at the instance's TOP level inserts its
content as SIBLINGS of those nodes — so everything such slots had
rendered leaked whenever an outer slot switched to a different
template. The agents detail pane (head + top-level tab/pane slots
swapping against the empty state) was the first in-tree shape to
trip it.

disposeChildState now recursively disposes the instance's own child
parts before removing the cloned nodes. Regression test covers the
switch in both directions; verified live in the browser on the
reported delete flow.
Every chat message was a stateless run: the client posted only the
current message, so a follow-up like "Yes, please" reached the model
with no idea what had been proposed. Reported symptom, and it is a
data-integrity bug, not a cosmetic one: an agent proposed a TL;DR for
post 973, the user approved, and the agent — starting from nothing —
searched, picked an unrelated post, and wrote to 614 instead.

The invoke route now accepts a  array of prior
{ role: 'user'|'agent', text } turns, capped at the 20 most recent ×
4000 chars, and the runner folds them into the composed prompt ahead
of the new message with an explicit instruction to resolve references
against the conversation rather than a fresh search. Client side, the
chat window's typed path now delegates to the same
invokeAgentIntoTranscript() the drop path uses, which snapshots the
transcript (skipping pending and error rows) before appending the new
message — so both intakes replay identically and neither can
regress independently.

Verified live: turn two of the reported flow now carries the proposed
post id into the prompt.
Two mutating abilities that complete the demo-facing toolset:

- desktop-mode/update-media: alt text / title / caption / description
  on an attachment, gated on the same edit capability wp-admin
  requires. The file itself is never touched. Unlocks accessibility
  agents (write alt text at drag-and-drop speed).
- desktop-mode/create-post: creates a NEW post or page with the
  status hard-forced to draft — it can never publish, whatever the
  model asks for. Authored by the calling user, so agent-created
  drafts carry agent attribution. Page creation additionally gates on
  edit_pages. Unlocks translation/derivative agents that produce
  reviewable drafts without touching any existing content.

Both are unannotated (mutating): invisible to the Copilot, reachable
only through an agent's explicit allowlist. Six new tests cover
registration annotations, the write paths, draft forcing, authorship,
and the capability denials.
Double-clicking an agent's tile on the desktop now starts a
conversation with the agent; human user tiles keep opening the
profile window. Built on a new per-FILE seam in the opener registry:
FileOpenerDef gains an optional appliesTo(file) predicate, honoured
by resolveOpener/getOpenersForType when a file is passed (the open
dispatcher now passes it). Predicate-bearing openers are excluded
from type-level listings where no file exists to test — the
default-apps settings tab never shows them. registerOpener's
normalization also had to learn the field; it silently dropped
unknown keys, which the new tests would have missed without the
failing-first run.

The built-in agent-chat opener (isDefault, sort 5, appliesTo
shape.isAgent) seeds the cross-bundle chat store via the shared
openAgentChatWindow() helper — extracted from the drop dispatch so
tile-open, drop, and chat all surface the window identically. The
user-file payload now inlines agentDescription so the chat header
shows the agent's when-to-use line without a REST roundtrip.

Verified live: the registry resolves agent-chat for agent files and
wp-user-profile for humans on the running site.
…le and desktop shortcuts

- Remove the remove-background extension, its abilities, tests, and
  every reference; the Photo Studio demo agent is gone with it.
- Wire the send-to trigger: agents with a send-to trigger appear as
  'Send to <agent>' entries in the site folder tile context menus
  (posts, pages, media, users), gated by the trigger's entityKinds.
  The users grid menu now runs the same tile-context-menu filter seam
  as posts and media. Registration is idempotent on the hooks bus
  because the bundle IIFE can execute twice (boot enqueue plus the
  native-window lazy loader).
- Agent chat: WhatsApp-style avatars on both sides of the
  conversation, a much wider in-flight bubble, a New chat button that
  clears the transcript, and markdown answers rendered via a new
  shared src/markdown.ts (extracted from the AI assistant, now with
  headings, thematic breaks, and per-line inline tokens so stray
  asterisks cannot pair across lines).
- Agents window: Open profile button (opens the user-edit window for
  the agent) and Send to Desktop button (creates a wallpaper tile on
  the first free grid cell; count-based slotting collided with moved
  tiles and buried existing icons).
- Trigger kinds catalogue now carries a wired flag; the Triggers pane
  renders unwired kinds (hook, endpoint, agent-to-agent) as disabled
  'coming soon' options.
…e desktop

- The in-flight chat bubble centers its label and spinner (the bubble
  is a flex column, so text-align alone left-aligned the spinner).
- Agent rows in the Agents section are draggable out as 'user'
  shortcuts via the shared attachTileDragOut helper: dropping a row on
  the wallpaper creates the same agent tile the Users grid drag
  produces. Attach is guarded per element so a repaint cannot stack a
  second pointerdown listener.
…replay cap to 50

- New desktop_mode_chat private post type: one post per conversation,
  post_author = the human, messages as JSON in post_content, agent id
  in meta. Strictly owner-only REST CRUD under
  /desktop-mode/v1/agents/conversations (list stays light, message
  bodies fetched per conversation); foreign rows read as 404 so ids
  cannot be probed. Caps: 100 conversations per user (filterable via
  desktop_mode_agent_conversation_cap, prune orders by modified with
  an ID tie-break so same-second creates cannot self-prune), 200
  messages per conversation, tool-call outputs dropped on store.
- Chat window gains a left sidebar: + New chat, past conversations
  (agent avatar + derived title, active highlight, hover delete with
  confirm), clicking a row reloads its transcript and re-targets the
  chat to that agent. The window default width grows to 760px.
- Auto-save after every completed exchange in the shared dispatcher
  (chat, drag, and send-to all persist); failures are swallowed so
  persistence can never break the conversation itself.
- History replay cap raised from 20 to 50 turns, filterable via
  desktop_mode_agent_history_turn_cap; both filters documented in
  docs/hooks-reference.md.
@epeicher epeicher changed the title AI Agents framework: user-backed agents, abilities runner, chat + drag-and-drop triggers, Remove Background extension AI Agents framework: agents as WordPress users, abilities runner, chat with persisted conversations, drag & drop and Send to triggers Jul 30, 2026
@epeicher epeicher self-assigned this Jul 30, 2026
Agents that need the user's confirmation now return renderable
buttons instead of asking for a typed reply.

- The runner constrains every final answer to a { text,
  call_to_actions } JSON schema via the AI Client's structured output
  (as_json_response), and a system-prompt appendix teaches the
  convention so existing agents pick it up without prompt edits. Each
  action carries id, label, style (primary/secondary/danger), and a
  reply: the literal message sent back as the user's next turn when
  its button is pressed.
- Parsing is lenient: answers that are not the JSON shape (pre-filter
  runtimes, providers that ignore the schema) pass through verbatim
  with no actions, so structured answers only ever degrade to the old
  behavior. Fenced JSON is tolerated. Sanitization caps 4 actions,
  40-char labels, 500-char replies, enforces the style enum.
- The chat window renders the actions as wpd-buttons under the
  agent's bubble. Only the latest message's buttons are live; pressing
  one posts its reply as a visible user message (the stored history
  shows exactly what was approved) and marks the message ctaUsed so
  reopened conversations render them disabled. Buttons persist with
  the conversation.
- Invoke results and the completed action now carry callToActions;
  conversation storage round-trips callToActions + ctaUsed.
@AllTerrainDeveloper

Copy link
Copy Markdown
Collaborator

Checking!

- New includes/agents/defaults.php ships a complete default roster:
  tl;dr, Comment Concierge, Localizer (author role), SEO Medic, and
  Alt Text Librarian — full system prompts, ability allowlists, and
  chat + send-to + drag triggers. Seeded once per site, and ONLY when
  the site has no agents at all; an install that already built its own
  roster gets the seeded flag without any rows. The hook wrapper runs
  on admin_init gated on edit_users so the seeder stays out of
  front-end requests, cron, and the PHPUnit bootstrap. Abilities that
  are not registered on the site (the ai/* family) are skipped by the
  runner at tool-build time, so allowlisting them costs nothing.
- The Agents section loading state uses the standard large loading
  logo (the clamp(96px, 14vw, 192px) scale curve shared with the
  preview loader) instead of the bare 48px spinner default.
@epeicher
epeicher merged commit 99bb36a into trunk Jul 31, 2026
5 checks passed
@epeicher
epeicher deleted the add/agents-phase-a branch July 31, 2026 10:10
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