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 +70 -3 3.1.4 → trunk 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),