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 +164 -11 3.0.53.2.1 View file →
@@ -1,12 +1,64 @@
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 -import { useWorkflowStore } from '@agent/state/workflows';
4 19 import { tools } from '@agent/workflows/workflows';
5 20 import { AI_HOST } from '@constants';
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,
@@ -30,11 +82,13 @@
30 82 const { failedWorkflows, context } = window.extAgentData;
31 83 const failed = failedWorkflows ?? new Set();
32 84 const filteredWorkflows = workflows.filter((wf) => !failed.has(wf.id));
33 85
34 - const { workflowHistory: pastWorkflows, block } = useWorkflowStore.getState();
86 + const block = useQuickEditStore.getState().agentBlock;
35 87
36 - const messages = useChatStore.getState().getMessagesForAI();
88 + const messages = useChatStore
89 + .getState()
90 + .getCurrentMessages({ includeTools: false });
37 91 const lastAssistantMessage = useChatStore
38 92 .getState()
39 93 .getLastAssistantMessage();
40 94
@@ -44,19 +98,22 @@
44 98 signal: options?.signal,
45 99 body: JSON.stringify({
46 100 ...reqDataBasics,
47 101 workflows: filteredWorkflows,
48 - previousAgentName: pastWorkflows.at(0)?.agentName,
49 102 previousWorkflow: {
103 + workflowId: lastAssistantMessage?.details?.workflowId,
104 + language: lastAssistantMessage?.details?.language,
50 105 lastMessage: lastAssistantMessage?.details?.content,
51 106 sessionId: lastAssistantMessage?.details?.sessionId,
52 - ...pastWorkflows?.at(0),
53 107 },
54 108 context,
55 109 agentContext: window.extAgentData.agentContext,
110 + wpAbilities: window.extAgentData.wpAbilities ?? [],
111 + clientTools: getClientTools(),
56 112 messages: messages.slice(-5),
57 113 hasBlock: Boolean(block), // todo: remove this
58 114 blockDetails: block,
115 + features: API_FEATURES,
59 116 ...options,
60 117 extra: extra(),
61 118 }),
62 119 });
@@ -76,9 +133,25 @@
76 133 return await response.json();
77 134 };
78 135
79 136 export const handleWorkflow = async ({ workflow, workflowData, options }) => {
80 - const messages = useChatStore.getState().getMessagesForAI();
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;
81 154 const response = await fetch(`${AI_HOST}/api/agent/handle-workflow`, {
82 155 method: 'POST',
83 156 headers: { 'Content-Type': 'application/json' },
84 157 signal: options?.signal,
@@ -84,13 +157,27 @@
84 157 signal: options?.signal,
85 158 body: JSON.stringify({
86 159 ...reqDataBasics,
87 160 workflow,
88 - workflowData,
89 - messages: messages,
161 + workflowData: data,
162 + messages: getCurrentMessages(),
163 + previousMessages: getMessagesFor(workflow?.id),
90 164 context: window.extAgentData.context,
91 165 agentContext: window.extAgentData.agentContext,
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(),
92 178 retry: options?.retry || false,
179 + features: API_FEATURES,
93 180 extra: extra(),
94 181 }),
95 182 });
96 183
@@ -97,8 +184,37 @@
97 184 if (!response.ok) throw new Error('Bad response from server');
98 185 return await response.json();
99 186 };
100 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 +
101 217 export const rateAnswer = ({ answerId, rating }) =>
102 218 fetch(`${AI_HOST}/api/agent/rate-workflow`, {
103 219 method: 'POST',
104 220 headers: { 'Content-Type': 'application/json' },
@@ -109,11 +225,48 @@
109 225 details: { source: 'agent', caller: 'rateAnswer', answerId, rating },
110 226 }),
111 227 );
112 228
113 -export const callTool = async ({ tool, inputs }) => {
114 - if (!tools[tool]) throw new Error(`Tool ${tool} not found`);
115 - return await tools[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 = [] }) => {
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) };
258 + // Ability tools are named after the ability and have no file; the generic
259 + // runner executes them. Key the result to its slot so the loop sees it filled.
260 + const isAbility = (window.extAgentData?.wpAbilities ?? []).some((category) =>
261 + category.abilities?.some((ability) => ability.name === tool),
262 + );
263 + if (isAbility) {
264 + return {
265 + [tool]: await tools['execute-ability']({ ability: tool, input: inputs }),
266 + };
267 + }
268 + throw new Error(`Tool ${tool} not found`);
116 269 };
117 270
118 271 export const recordAgentActivity = ({ action, sessionId, value = {} }) => {
119 272 return fetch(`${AI_HOST}/api/agent/activities`, {