PluginProbe
Extendify / trunk
Extendify vtrunk
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
← All changes | src/Agent/api.js +140 -2 3.1.3 → trunk View file →
@@ -1,4 +1,20 @@
1 +import { abilityDescriptors, getAbilities } from '@agent/abilities/abilities';
2 +import { findBlockEl, scopeOf } from '@agent/lib/block-el';
3 +import { buildBlockSchema } from '@agent/lib/block-schema';
4 +import {
5 + classifyBlockEdit,
6 + IGNORED_BLOCKS,
7 +} from '@agent/lib/classify-block-edit';
8 +import { getClientTools } from '@agent/lib/client-tools';
9 +import { isExtendableHeader } from '@agent/lib/extendable-header';
10 +import { INSERTABLE_BLOCK_TYPES } from '@agent/lib/insertable-blocks';
11 +import { ensureCoreBlocksRegistered } from '@agent/lib/register-blocks';
12 +import {
13 + buildSubtreeManifest,
14 + buildSubtreeTree,
15 +} from '@agent/lib/subtree-manifest';
16 +import { activeCanvasStep } from '@agent/state/canvas';
1 17 import { useChatStore } from '@agent/state/chat';
2 18 import { useGlobalStore } from '@agent/state/global';
3 19 import { tools } from '@agent/workflows/workflows';
4 20 import { AI_HOST } from '@constants';
@@ -4,9 +20,45 @@
4 20 import { AI_HOST } from '@constants';
5 21 import { useQuickEditStore } from '@quick-edit/state/store';
6 22 import { digest } from '@shared/api/digest';
7 23 import { reqDataBasics } from '@shared/lib/data';
24 +import { getBlockType } from '@wordpress/blocks';
8 25
26 +// page-templates:v1 declares that find-blocks and stage-block reach into parts.
27 +export const API_FEATURES = ['qa-tool', 'page-templates:v1'];
28 +
29 +const rootFor = (block) =>
30 + block?.id && block?.target
31 + ? findBlockEl(block.id, document, scopeOf(block))
32 + : null;
33 +
34 +// If greater than 5 blocks the agent will narrow the scope
35 +const EAGER_LOAD_MAX = 5;
36 +
37 +// The staged selection's attribute schemas, with the install's palette baked in.
38 +export const blockSchemasFor = async (block) => {
39 + const root = rootFor(block);
40 + const { bucket } = classifyBlockEdit({ block, root });
41 + const types =
42 + bucket === 'single'
43 + ? [block.blockType]
44 + : IGNORED_BLOCKS.has(block?.blockType)
45 + ? []
46 + : [...new Set(buildSubtreeManifest(root).map(({ type }) => type))];
47 + if (!types.length || types.length > EAGER_LOAD_MAX) return [];
48 + await ensureCoreBlocksRegistered();
49 + return types
50 + .map((type) => ({ type, schema: buildBlockSchema(getBlockType(type)) }))
51 + .filter(({ schema }) => schema);
52 +};
53 +
54 +// Only consumed for a multi-block edit; inert for single/combo.
55 +export const subtreeManifestFor = (block) =>
56 + buildSubtreeManifest(rootFor(block));
57 +
58 +// Selection hierarchy for the backend's positional edits.
59 +export const subtreeTreeFor = (block) => buildSubtreeTree(rootFor(block));
60 +
9 61 const extra = () => {
10 62 const { x, y, width, height } = useGlobalStore.getState();
11 63 return {
12 64 userAgent: window?.navigator?.userAgent,
@@ -55,11 +107,13 @@
55 107 },
56 108 context,
57 109 agentContext: window.extAgentData.agentContext,
58 110 wpAbilities: window.extAgentData.wpAbilities ?? [],
111 + clientTools: getClientTools(),
59 112 messages: messages.slice(-5),
60 113 hasBlock: Boolean(block), // todo: remove this
61 114 blockDetails: block,
115 + features: API_FEATURES,
62 116 ...options,
63 117 extra: extra(),
64 118 }),
65 119 });
@@ -80,8 +134,24 @@
80 134 };
81 135
82 136 export const handleWorkflow = async ({ workflow, workflowData, options }) => {
83 137 const { getCurrentMessages, getMessagesFor } = useChatStore.getState();
138 + const block = useQuickEditStore.getState().agentBlock;
139 + const isBlockPatching =
140 + workflow?.id === 'block-patching' ||
141 + (workflow?.id === 'select-block' && Boolean(block));
142 + // Spent once the manifest flows: patching never reads this workflow's inputs.
143 + const { blockSearch: _spent, ...carried } = workflowData ?? {};
144 + // Schemas: pinned up front when the selection is small enough; otherwise
145 + // empty until get-block-schemas fetches them into the same field.
146 + const data = isBlockPatching
147 + ? {
148 + ...carried,
149 + blockSchemas: workflowData?.blockSchemas?.length
150 + ? workflowData.blockSchemas
151 + : await blockSchemasFor(block),
152 + }
153 + : workflowData;
84 154 const response = await fetch(`${AI_HOST}/api/agent/handle-workflow`, {
85 155 method: 'POST',
86 156 headers: { 'Content-Type': 'application/json' },
87 157 signal: options?.signal,
@@ -87,15 +157,27 @@
87 157 signal: options?.signal,
88 158 body: JSON.stringify({
89 159 ...reqDataBasics,
90 160 workflow,
91 - workflowData,
161 + workflowData: data,
92 162 messages: getCurrentMessages(),
93 163 previousMessages: getMessagesFor(workflow?.id),
94 164 context: window.extAgentData.context,
95 165 agentContext: window.extAgentData.agentContext,
96 166 wpAbilities: window.extAgentData.wpAbilities ?? [],
167 + clientTools: getClientTools(),
168 + // The manifest is the backend's block-patching signal — never send it
169 + // for other workflows or they'd be routed into the patching handler.
170 + blockManifest: isBlockPatching ? subtreeManifestFor(block) : [],
171 + blockTree: isBlockPatching ? subtreeTreeFor(block) : [],
172 + // The plugin owns the add catalog so old fielded builds are never
173 + // offered a type they can't build.
174 + insertableBlockTypes: isBlockPatching ? INSERTABLE_BLOCK_TYPES : [],
175 + // The patcher writes dead CSS for these switches and claims success.
176 + isExtendableHeader:
177 + isBlockPatching && block?.template === 'header' && isExtendableHeader(),
97 178 retry: options?.retry || false,
179 + features: API_FEATURES,
98 180 extra: extra(),
99 181 }),
100 182 });
101 183
@@ -102,8 +184,37 @@
102 184 if (!response.ok) throw new Error('Bad response from server');
103 185 return await response.json();
104 186 };
105 187
188 +export const handleCanvas = async ({
189 + toolId,
190 + sessionId,
191 + abilities,
192 + options,
193 +}) => {
194 + const { getCurrentMessages, getMessagesFor } = useChatStore.getState();
195 + const { values } = activeCanvasStep();
196 + const response = await fetch(`${AI_HOST}/api/agent/handle-canvas`, {
197 + method: 'POST',
198 + headers: { 'Content-Type': 'application/json' },
199 + signal: options?.signal,
200 + body: JSON.stringify({
201 + ...reqDataBasics,
202 + toolId,
203 + values,
204 + abilities: abilityDescriptors(getAbilities(abilities)),
205 + messages: getCurrentMessages(),
206 + previousMessages: getMessagesFor(toolId),
207 + context: window.extAgentData.context,
208 + sessionId,
209 + extra: extra(),
210 + }),
211 + });
212 +
213 + if (!response.ok) throw new Error('Bad response from server');
214 + return await response.json();
215 +};
216 +
106 217 export const rateAnswer = ({ answerId, rating }) =>
107 218 fetch(`${AI_HOST}/api/agent/rate-workflow`, {
108 219 method: 'POST',
109 220 headers: { 'Content-Type': 'application/json' },
@@ -114,10 +225,37 @@
114 225 details: { source: 'agent', caller: 'rateAnswer', answerId, rating },
115 226 }),
116 227 );
117 228
118 -export const callTool = async ({ tool, inputs }) => {
229 +// An error with no message serializes to {}, which the model reads as an empty success.
230 +const reasonFor = async (error) => {
231 + // An unparsed apiFetch rejects with the Response, whose body holds the reason.
232 + if (typeof error?.json === 'function') {
233 + const body = await error.json().catch(() => null);
234 + return body?.message ?? `The site answered ${error.status}.`;
235 + }
236 +
237 + return (
238 + error?.error ?? error?.message ?? 'The ability failed without saying why.'
239 + );
240 +};
241 +
242 +// A throw would reach the model outside the tool's slot, reading as no result.
243 +const runAbility = async (ability, inputs) => {
244 + try {
245 + return await ability.execute(inputs);
246 + } catch (error) {
247 + return { error: await reasonFor(error) };
248 + }
249 +};
250 +
251 +export const callTool = async ({ tool, inputs, abilities = [] }) => {
119 252 if (tools[tool]) return await tools[tool](inputs);
253 + // Ours wins a name collision, so a duplicate WP ability can't change what runs.
254 + const ours =
255 + getAbilities(abilities).find(({ name }) => name === tool) ??
256 + getAbilities([tool])[0];
257 + if (ours) return { [tool]: await runAbility(ours, inputs) };
120 258 // Ability tools are named after the ability and have no file; the generic
121 259 // runner executes them. Key the result to its slot so the loop sees it filled.
122 260 const isAbility = (window.extAgentData?.wpAbilities ?? []).some((category) =>
123 261 category.abilities?.some((ability) => ability.name === tool),