PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
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 +70 -3 3.1.43.2.1 View file →
@@ -1,4 +1,6 @@
1 +import { abilityDescriptors, getAbilities } from '@agent/abilities/abilities';
2 +import { findBlockEl, scopeOf } from '@agent/lib/block-el';
1 3 import { buildBlockSchema } from '@agent/lib/block-schema';
2 4 import {
3 5 classifyBlockEdit,
4 6 IGNORED_BLOCKS,
@@ -3,8 +5,9 @@
3 5 classifyBlockEdit,
4 6 IGNORED_BLOCKS,
5 7 } from '@agent/lib/classify-block-edit';
6 8 import { getClientTools } from '@agent/lib/client-tools';
9 +import { isExtendableHeader } from '@agent/lib/extendable-header';
7 10 import { INSERTABLE_BLOCK_TYPES } from '@agent/lib/insertable-blocks';
8 11 import { ensureCoreBlocksRegistered } from '@agent/lib/register-blocks';
9 12 import {
10 13 buildSubtreeManifest,
@@ -9,8 +12,9 @@
9 12 import {
10 13 buildSubtreeManifest,
11 14 buildSubtreeTree,
12 15 } from '@agent/lib/subtree-manifest';
16 +import { activeCanvasStep } from '@agent/state/canvas';
13 17 import { useChatStore } from '@agent/state/chat';
14 18 import { useGlobalStore } from '@agent/state/global';
15 19 import { tools } from '@agent/workflows/workflows';
16 20 import { AI_HOST } from '@constants';
@@ -18,11 +22,14 @@
18 22 import { digest } from '@shared/api/digest';
19 23 import { reqDataBasics } from '@shared/lib/data';
20 24 import { getBlockType } from '@wordpress/blocks';
21 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 +
22 29 const rootFor = (block) =>
23 30 block?.id && block?.target
24 - ? document.querySelector(`[${block.target}="${block.id}"]`)
31 + ? findBlockEl(block.id, document, scopeOf(block))
25 32 : null;
26 33
27 34 // If greater than 5 blocks the agent will narrow the scope
28 35 const EAGER_LOAD_MAX = 5;
@@ -104,8 +111,9 @@
104 111 clientTools: getClientTools(),
105 112 messages: messages.slice(-5),
106 113 hasBlock: Boolean(block), // todo: remove this
107 114 blockDetails: block,
115 + features: API_FEATURES,
108 116 ...options,
109 117 extra: extra(),
110 118 }),
111 119 });
@@ -163,10 +171,13 @@
163 171 blockTree: isBlockPatching ? subtreeTreeFor(block) : [],
164 172 // The plugin owns the add catalog so old fielded builds are never
165 173 // offered a type they can't build.
166 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(),
167 178 retry: options?.retry || false,
168 - features: ['qa-tool'],
179 + features: API_FEATURES,
169 180 extra: extra(),
170 181 }),
171 182 });
172 183
@@ -173,8 +184,37 @@
173 184 if (!response.ok) throw new Error('Bad response from server');
174 185 return await response.json();
175 186 };
176 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 +
177 217 export const rateAnswer = ({ answerId, rating }) =>
178 218 fetch(`${AI_HOST}/api/agent/rate-workflow`, {
179 219 method: 'POST',
180 220 headers: { 'Content-Type': 'application/json' },
@@ -185,10 +225,37 @@
185 225 details: { source: 'agent', caller: 'rateAnswer', answerId, rating },
186 226 }),
187 227 );
188 228
189 -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 = [] }) => {
190 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) };
191 258 // Ability tools are named after the ability and have no file; the generic
192 259 // runner executes them. Key the result to its slot so the loop sees it filled.
193 260 const isAbility = (window.extAgentData?.wpAbilities ?? []).some((category) =>
194 261 category.abilities?.some((ability) => ability.name === tool),