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 +160 -7 3.1.1 → 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,
@@ -32,9 +84,11 @@
32 84 const filteredWorkflows = workflows.filter((wf) => !failed.has(wf.id));
33 85
34 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
@@ -52,11 +106,14 @@
52 106 sessionId: lastAssistantMessage?.details?.sessionId,
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`, {