@wix/mcp 1.0.93 → 1.0.95

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +2 -2
  2. package/build/bin-standalone.js +1200 -455
  3. package/build/cjs/index.cjs +3237 -1264
  4. package/build/cjs/toolkit.cjs +2177 -469
  5. package/build/dts/api-call/headless-release.d.ts +56 -0
  6. package/build/dts/api-call/headless-release.js +135 -0
  7. package/build/dts/api-call/index.d.ts +9 -1
  8. package/build/dts/api-call/index.js +19 -10
  9. package/build/dts/api-call/site-files.d.ts +42 -0
  10. package/build/dts/api-call/site-files.js +290 -0
  11. package/build/dts/api-call/upload-headless-website-files.d.ts +118 -0
  12. package/build/dts/api-call/upload-headless-website-files.js +624 -0
  13. package/build/dts/api-call/upload-image-tool.d.ts +2 -0
  14. package/build/dts/api-call/upload-image-tool.js +8 -4
  15. package/build/dts/bin.d.ts +1 -1
  16. package/build/dts/bin.js +4 -5
  17. package/build/dts/code-mode/attachments.js +1 -1
  18. package/build/dts/code-mode/index.d.ts +6 -1
  19. package/build/dts/code-mode/index.js +47 -13
  20. package/build/dts/code-mode/truncate-response.js +5 -1
  21. package/build/dts/config/default-config.d.ts +35 -9
  22. package/build/dts/config/default-config.js +348 -207
  23. package/build/dts/config/file-params.d.ts +22 -0
  24. package/build/dts/config/file-params.js +24 -0
  25. package/build/dts/config/param-descriptions.d.ts +14 -2
  26. package/build/dts/config/param-descriptions.js +34 -15
  27. package/build/dts/constants.d.ts +1 -0
  28. package/build/dts/constants.js +1 -0
  29. package/build/dts/docs/docs.d.ts +16 -3
  30. package/build/dts/docs/docs.js +211 -39
  31. package/build/dts/docs/fetch-article.d.ts +21 -0
  32. package/build/dts/docs/fetch-article.js +70 -5
  33. package/build/dts/docs/get-to-know.d.ts +7 -2
  34. package/build/dts/docs/get-to-know.js +37 -15
  35. package/build/dts/docs/mintlify-docs.d.ts +121 -0
  36. package/build/dts/docs/mintlify-docs.js +391 -0
  37. package/build/dts/docs/semanticSearch.d.ts +2 -1
  38. package/build/dts/docs/semanticSearch.js +14 -7
  39. package/build/dts/docs/start-deploy-website.d.ts +7 -6
  40. package/build/dts/docs/start-deploy-website.js +13 -8
  41. package/build/dts/docs/start-headless-build.d.ts +17 -0
  42. package/build/dts/docs/start-headless-build.js +60 -0
  43. package/build/dts/graphql/index.js +3 -1
  44. package/build/dts/import-claude-design-tool/index.js +2 -2
  45. package/build/dts/index.d.ts +3 -2
  46. package/build/dts/index.js +3 -2
  47. package/build/dts/infra/redact-personal-data.d.ts +4 -0
  48. package/build/dts/infra/redact-personal-data.js +79 -0
  49. package/build/dts/site-widget-tools/constants.d.ts +49 -1
  50. package/build/dts/site-widget-tools/constants.js +64 -7
  51. package/build/dts/site-widget-tools/index.d.ts +1 -0
  52. package/build/dts/site-widget-tools/index.js +1 -0
  53. package/build/dts/site-widget-tools/pick-images-tool/index.d.ts +14 -0
  54. package/build/dts/site-widget-tools/pick-images-tool/index.js +138 -0
  55. package/build/dts/site-widget-tools/search-site-templates-tool/index.js +33 -9
  56. package/build/dts/site-widget-tools/site-builder-tool/index.js +22 -2
  57. package/build/dts/site-widget-tools/site-creation-tool/index.d.ts +7 -1
  58. package/build/dts/site-widget-tools/site-creation-tool/index.js +22 -51
  59. package/build/dts/site-widget-tools/suggested-domain-tool/index.js +6 -3
  60. package/build/dts/support/index.d.ts +4 -0
  61. package/build/dts/support/index.js +13 -7
  62. package/build/dts/toolkit-entry.d.cts +19 -12
  63. package/build/dts/toolkit-entry.d.ts +19 -12
  64. package/build/dts/toolkit.d.ts +2 -2
  65. package/build/dts/toolkit.js +23 -7
  66. package/build/dts/wix-mcp-server.d.ts +11 -0
  67. package/build/dts/wix-mcp-server.js +28 -6
  68. package/build/dts/wixel/constants.d.ts +8 -1
  69. package/build/dts/wixel/constants.js +8 -1
  70. package/build/dts/wixel/index.d.ts +1 -1
  71. package/build/dts/wixel/index.js +2 -2
  72. package/build/dts/wixel/wixel-a2a-client.d.ts +83 -0
  73. package/build/dts/wixel/wixel-a2a-client.js +166 -0
  74. package/build/dts/wixel/wixel-bi.d.ts +2 -1
  75. package/build/dts/wixel/wixel-bi.js +2 -1
  76. package/build/dts/wixel/{external-agent-api.d.ts → wixel-session-entries.d.ts} +11 -33
  77. package/build/dts/wixel/{external-agent-api.js → wixel-session-entries.js} +33 -54
  78. package/build/dts/wixel/wixel-session-tools.d.ts +21 -26
  79. package/build/dts/wixel/wixel-session-tools.js +494 -599
  80. package/build/esm/index.js +3335 -1376
  81. package/build/esm/toolkit.js +2273 -565
  82. package/package.json +4 -4
  83. package/build/dts/docs/create-site-guide.d.ts +0 -3
  84. package/build/dts/docs/create-site-guide.js +0 -109
@@ -5,12 +5,15 @@ import { getWidgetHtml } from '../widget-manifest.js';
5
5
  import { RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server';
6
6
  import { WIXEL_WIDGET_URI, WIXEL_MCP_CSP } from './constants.js';
7
7
  import { getStylePresetsByIds, getAsset, queryAssets } from './api.js';
8
- import { startWixelSession, sendWixelSessionMessage, listWixelSessionEvents, getWixelPendingQuestion, getWixelSessionActivity, getWixelSessionProjectId } from './external-agent-api.js';
8
+ import { getWixelTask, isTerminalTaskState, sendWixelMessage, summarizeTask, WixelA2AError, WIXEL_MESSAGE_MAX_LENGTH } from './wixel-a2a-client.js';
9
+ import { getWixelPendingQuestion, getWixelSessionActivity, getWixelSessionProjectId } from './wixel-session-entries.js';
9
10
  import { registerAppTool } from '@modelcontextprotocol/ext-apps/server';
10
- import { isValidUuid } from './utils.js';
11
- // MCP tools backed by the wixel-agent-service external-session API
12
- // (WIXEL_AGENT_EXTERNAL_API handoff): start_wixel_session / SendMessage /
13
- // ListEvents with cursor-based inbox polling.
11
+ // MCP tools backed by the Wixel agent's A2A endpoint: send_wixel_message
12
+ // (SendMessage) starts a conversation or adds a turn and returns the turn as a
13
+ // task; get_wixel_task (GetTask) reports it. A conversation is a contextId, a
14
+ // turn a taskId — both opaque. A question the agent parks on is read from the
15
+ // session's entries log (wixel-session-entries.ts), which a task does not
16
+ // carry.
14
17
  const TOOL_META = {
15
18
  ui: { visibility: ['app', 'model'] }
16
19
  };
@@ -80,25 +83,6 @@ export function collectDecks(projectAssets, deckIds) {
80
83
  }
81
84
  return decks;
82
85
  }
83
- /**
84
- * A reply that ends by asking the user something (a plan to approve, a
85
- * choice to make) is not a delivery even when it links media — an animation
86
- * plan links the source image it will animate. Asset-id markers ("Asset
87
- * ready for … (id)") only appear on real deliveries; media URLs alone need
88
- * this check.
89
- */
90
- function isQuestionReply(text) {
91
- return /\?\s*$/.test(text.trim());
92
- }
93
- // Image/video replies often carry no asset-id marker — just the media URL in
94
- // the text: a wixstatic file, a wixmp render (img-wixmp-*/wixmp-* hosts), or a
95
- // links.wixel.com short link that redirects to one. Those links are the
96
- // deliverable too. Keep the hosts in step with parseWixelMediaUrl, or a URL
97
- // the resolver would accept never gets extracted in the first place.
98
- function extractMediaUrls(text) {
99
- const matches = text.match(/https:\/\/(?:(?:static|video)\.wixstatic\.com|[a-z0-9-]+\.wixmp\.com|links\.wixel\.com\/link)\/[^\s)\]"'<>]+/g);
100
- return matches ? Array.from(new Set(matches)) : [];
101
- }
102
86
  /**
103
87
  * Run `fn` over `items` with at most `limit` in flight. `assetIds`/`mediaUrls`
104
88
  * arrive from the model, so an unbounded Promise.all would let one call fan out
@@ -202,84 +186,59 @@ async function resolveWixelShortLink(url) {
202
186
  }
203
187
  return null;
204
188
  }
205
- // Terminal replies carry the created asset ids two ways: an HTML comment,
206
- // <!-- wixel-agent-assets:["<assetId>", ...] -->, or named inline as
207
- // `Asset ready for "<name>" (<assetId>)`. Both forms matter — without an id
208
- // there is no entity to resolve, and a deck falls back to the one URL the
209
- // reply does carry, which is its PDF export rather than its page images.
210
- // Anchored on that phrasing and UUID-checked so a turnId can't slip in.
211
- export function extractAssetIds(text) {
212
- const ids = [];
213
- const marker = text.match(/wixel-agent-assets:(\[[^\]]*\])/);
214
- if (marker) {
215
- try {
216
- const parsed = JSON.parse(marker[1]);
217
- if (Array.isArray(parsed)) {
218
- ids.push(...parsed.filter((id) => typeof id === 'string'));
219
- }
220
- }
221
- catch (error) {
222
- console.warn('[Wixel] unparsable asset marker', error.message);
223
- }
224
- }
225
- for (const match of text.matchAll(/Asset ready for\b[^\n]*?\(\s*([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})\s*\)/gi)) {
226
- ids.push(match[1]);
227
- }
228
- return Array.from(new Set(ids.filter(isValidUuid)));
229
- }
230
189
  const QUESTION_TOOL_META = {
231
190
  ui: {
232
191
  resourceUri: WIXEL_WIDGET_URI,
233
192
  visibility: ['app', 'model']
234
193
  }
235
194
  };
236
- const MAX_LIVE_CARD_HANDLES = 500;
195
+ const MAX_LIVE_CARD_CONTEXTS = 500;
237
196
  // A card that never reported done (unmounted, tab closed, timed out) must not
238
- // lock the agent out of the handle forever.
197
+ // lock the agent out of the conversation forever.
239
198
  const LIVE_CARD_TTL_MS = 30 * 60 * 1000;
240
199
  export function createLiveCardRegistry() {
241
200
  const records = new Map();
242
- const get = (handle) => records.get(handle);
243
- const forHandle = (handle) => {
244
- let record = records.get(handle);
201
+ const get = (contextId) => records.get(contextId);
202
+ const forContext = (contextId) => {
203
+ let record = records.get(contextId);
245
204
  if (!record) {
246
- if (records.size >= MAX_LIVE_CARD_HANDLES) {
205
+ if (records.size >= MAX_LIVE_CARD_CONTEXTS) {
247
206
  const oldest = records.keys().next().value;
248
207
  if (oldest !== undefined)
249
208
  records.delete(oldest);
250
209
  }
251
210
  record = {
252
- turns: new Map(),
253
- finishedTurns: new Set(),
211
+ tasks: new Map(),
212
+ finishedTasks: new Set(),
254
213
  shownAssets: new Set(),
255
- lastSentTurn: null
214
+ lastSentTask: null
256
215
  };
257
- records.set(handle, record);
216
+ records.set(contextId, record);
258
217
  }
259
218
  return record;
260
219
  };
261
220
  /**
262
- * Whether the turn the agent last sent is being followed by a live card
263
- * that has not finished. Only the card's own polls mark a turn finished,
264
- * so when the card is gone this also consults the inbox: a terminal event
265
- * for the turn ends the card's claim, as does the TTL.
221
+ * Whether the task last sent in the conversation is being followed by a
222
+ * live card that has not finished. Only the card's own polls mark a task
223
+ * finished, so when the card is gone this also asks for the task: a
224
+ * terminal state ends the card's claim, as does the TTL.
266
225
  */
267
- const isActive = async (handle, params) => {
268
- const record = records.get(handle);
269
- const turnId = record?.lastSentTurn;
270
- if (!record || !turnId)
226
+ const isActive = async (contextId, isTerminal) => {
227
+ const record = records.get(contextId);
228
+ const taskId = record?.lastSentTask;
229
+ if (!record || !taskId)
271
230
  return false;
272
- const renderedAt = record.turns.get(turnId);
273
- if (renderedAt === undefined || record.finishedTurns.has(turnId)) {
231
+ const renderedAt = record.tasks.get(taskId);
232
+ if (renderedAt === undefined || record.finishedTasks.has(taskId)) {
274
233
  return false;
275
234
  }
276
235
  if (Date.now() - renderedAt > LIVE_CARD_TTL_MS) {
277
- record.finishedTurns.add(turnId);
236
+ record.finishedTasks.add(taskId);
278
237
  return false;
279
238
  }
280
239
  try {
281
- if (await hasTerminalEvent({ ...params, handle, turnId })) {
282
- record.finishedTurns.add(turnId);
240
+ if (await isTerminal(taskId)) {
241
+ record.finishedTasks.add(taskId);
283
242
  return false;
284
243
  }
285
244
  }
@@ -289,29 +248,78 @@ export function createLiveCardRegistry() {
289
248
  }
290
249
  return true;
291
250
  };
292
- return { get, forHandle, isActive };
251
+ return { get, forContext, isActive };
293
252
  }
294
- // Replays the inbox from the start (explicit cursor, so the agent's remembered
295
- // cursor never moves) and reports whether the turn has its reply/failed event.
296
- async function hasTerminalEvent(params) {
297
- const inbox = await listWixelSessionEvents({
298
- authHeaders: params.authHeaders,
299
- conductorOverride: params.conductorOverride,
300
- handle: params.handle,
301
- metaSiteId: params.metaSiteId,
302
- since: '0'
303
- });
304
- if (inbox.error) {
305
- throw new Error(`${inbox.error} (${inbox.code ?? 'no code'})`);
253
+ /**
254
+ * A terminal check for isActive that reuses a task already fetched in this
255
+ * call instead of asking for it again.
256
+ */
257
+ function terminalCheck(base, known) {
258
+ return async (taskId) => {
259
+ const task = known && known.id === taskId
260
+ ? known
261
+ : await getWixelTask({ ...base, taskId });
262
+ return isTerminalTaskState(task.status?.state);
263
+ };
264
+ }
265
+ /**
266
+ * GetTask for a task the caller names together with its site. The service
267
+ * already refuses a task outside the caller's own sessions and the site they
268
+ * were created on; this repeats the site half here, so a task is never shown
269
+ * under a metaSiteId it does not carry. Answers like the service: not found.
270
+ */
271
+ async function getSiteTask({ metaSiteId, ...params }) {
272
+ const task = await getWixelTask(params);
273
+ const taskSite = summarizeTask(task).metaSiteId;
274
+ if (taskSite && taskSite !== metaSiteId) {
275
+ throw new WixelA2AError(404, 'Unknown task id');
306
276
  }
307
- return (inbox.events ?? []).some((event) => event.turnId === params.turnId &&
308
- (event.type === 'reply' || event.type === 'failed'));
277
+ return task;
309
278
  }
279
+ /** A tool result for an error the Wixel agent's A2aService answered with. */
280
+ function a2aErrorResult(prefix, error) {
281
+ return {
282
+ isError: true,
283
+ content: [
284
+ {
285
+ type: 'text',
286
+ text: `${prefix}: ${error.message}${error.status ? ` (HTTP ${error.status})` : ''}`
287
+ }
288
+ ],
289
+ structuredContent: {
290
+ success: false,
291
+ error: error.message,
292
+ status: error.status
293
+ }
294
+ };
295
+ }
296
+ const CONTEXT_MISMATCH_TEXT = 'contextId does not belong to taskId. Pass the contextId and taskId exactly as send_wixel_message returned them.';
297
+ function contextMismatchResult() {
298
+ return {
299
+ isError: true,
300
+ content: [{ type: 'text', text: CONTEXT_MISMATCH_TEXT }],
301
+ structuredContent: { success: false, error: 'contextId mismatch' }
302
+ };
303
+ }
304
+ const END_TURN_SILENTLY = `End your turn now WITHOUT writing any message — no summary, no "Wixel is working on it", nothing: the card already says it, and a sentence under it is redundant. If your client forces a reply, use at most two words.`;
310
305
  /** McpAppsBiProvider builds the widget's BI logger from
311
306
  * widgetPayload.extras.biContext; without it every widget event is dropped. */
312
307
  function widgetResultMeta(server) {
313
308
  return { widgetPayload: { extras: server.getWidgetExtras() } };
314
309
  }
310
+ const taskIdInput = z
311
+ .string()
312
+ .min(1)
313
+ .max(512)
314
+ .describe('taskId returned by send_wixel_message');
315
+ const contextIdInput = z
316
+ .string()
317
+ .min(1)
318
+ .max(512)
319
+ .describe('contextId returned by send_wixel_message');
320
+ const metaSiteIdInput = z
321
+ .string()
322
+ .describe('metaSiteId returned by send_wixel_message');
315
323
  export function addGetWixelPendingQuestionTool(server, authStrategy, options = {}) {
316
324
  const liveCards = options.liveCards ?? createLiveCardRegistry();
317
325
  const { getSiteAuthHeaders } = authStrategy;
@@ -338,17 +346,16 @@ export function addGetWixelPendingQuestionTool(server, authStrategy, options = {
338
346
  });
339
347
  registerAppTool(server, 'get_wixel_pending_question', {
340
348
  title: 'Get pending Wixel question',
341
- description: `Fetch the structured question the Wixel agent asked in its latest turn (outline review, style selection, confirmation, choices).
342
- Call this after a reply event whenever the reply asks the user something. In widget-capable clients this renders an interactive question widget the user can answer directly; otherwise present the returned questions as text and send the user's answer via send_wixel_message.`,
349
+ description: `Fetch the structured question the Wixel agent asked in a turn (outline review, style selection, confirmation, choices).
350
+ Call this when get_wixel_task flags a question, or its reply asks the user something. In widget-capable clients this renders an interactive question widget the user can answer directly; otherwise present the returned questions as text and send the user's answer via send_wixel_message.`,
343
351
  inputSchema: {
344
- handle: z.string().describe('Session handle from start_wixel_session'),
345
- metaSiteId: z
346
- .string()
347
- .describe('Meta site ID from start_wixel_session'),
352
+ contextId: contextIdInput,
353
+ taskId: taskIdInput,
354
+ metaSiteId: metaSiteIdInput,
348
355
  questionId: z
349
356
  .string()
350
357
  .optional()
351
- .describe('Pass the questionId when list_wixel_events flagged one — it binds the rendered widget to that question and returns null if it is no longer pending.')
358
+ .describe('Pass the questionId when get_wixel_task flagged one — it binds the rendered widget to that question and returns null if it is no longer pending.')
352
359
  },
353
360
  annotations: {
354
361
  readOnlyHint: true,
@@ -357,31 +364,38 @@ Call this after a reply event whenever the reply asks the user something. In wid
357
364
  },
358
365
  _meta: QUESTION_TOOL_META
359
366
  }, async (args, extra) => {
360
- setBi(extra, {
361
- siteId: args.metaSiteId,
362
- conversationId: args.handle
363
- });
367
+ setBi(extra, { siteId: args.metaSiteId });
364
368
  // Resolve auth before the try block so UnauthorizedError propagates to
365
369
  // wrapToolCallback with the OAuth re-auth metadata (see auth/index.ts).
366
370
  const authHeaders = await getSiteAuthHeaders(args.metaSiteId);
371
+ const base = {
372
+ authHeaders,
373
+ conductorOverride: options.conductorOverride
374
+ };
367
375
  try {
368
- // The external API returns handle === sessionId; the internal lookups
369
- // verify the id against the caller's own sessions listing and fail
370
- // closed if the contract ever diverges (see isSessionVisibleToCaller).
371
- let pending = await getWixelPendingQuestion({
372
- authHeaders,
373
- sessionId: args.handle,
374
- conductorOverride: options.conductorOverride
376
+ // The session is read from the task the agent itself vouches for;
377
+ // the entries reads verify it against the caller's own sessions
378
+ // listing as well (see isSessionVisibleToCaller).
379
+ const task = await getSiteTask({
380
+ ...base,
381
+ taskId: args.taskId,
382
+ metaSiteId: args.metaSiteId
375
383
  });
384
+ if (task.contextId !== args.contextId)
385
+ return contextMismatchResult();
386
+ const { sessionId, turnId } = summarizeTask(task);
387
+ setBi(extra, {
388
+ siteId: args.metaSiteId,
389
+ ...(sessionId ? { conversationId: sessionId } : {})
390
+ });
391
+ let pending = sessionId
392
+ ? await getWixelPendingQuestion({ ...base, sessionId, turnId })
393
+ : null;
376
394
  // The live card renders this question inline; a second, standalone
377
395
  // question widget would put it in the chat twice.
378
396
  if (pending &&
379
397
  options.supportsWidgets?.() === true &&
380
- (await liveCards.isActive(args.handle, {
381
- authHeaders,
382
- conductorOverride: options.conductorOverride,
383
- metaSiteId: args.metaSiteId
384
- }))) {
398
+ (await liveCards.isActive(task.contextId, terminalCheck(base, task)))) {
385
399
  console.log('[WixelResponseDump]', JSON.stringify({
386
400
  tool: 'get_wixel_pending_question',
387
401
  args,
@@ -393,7 +407,7 @@ Call this after a reply event whenever the reply asks the user something. In wid
393
407
  {
394
408
  type: 'text',
395
409
  text: `The live result card already shows this question to the user and sends the answer to Wixel itself.
396
- <agent_instruction>Do NOT present the question and do NOT poll list_wixel_events — the card follows the turn. Do not tell the user to answer either — the card is self-explanatory. End your turn now WITHOUT writing any message — no summary, no "Wixel is working on it", nothing: the card already says it, and a sentence under it is redundant. If your client forces a reply, use at most two words.</agent_instruction>`
410
+ <agent_instruction>Do NOT present the question and do NOT poll get_wixel_task — the card follows the turn. Do not tell the user to answer either — the card is self-explanatory. ${END_TURN_SILENTLY}</agent_instruction>`
397
411
  }
398
412
  ],
399
413
  structuredContent: { success: true, pendingQuestion: null }
@@ -430,18 +444,19 @@ Call this after a reply event whenever the reply asks the user something. In wid
430
444
  .join('\n');
431
445
  const responseText = widgetShown
432
446
  ? `An interactive question widget is displayed to the user (questionId: ${pending.toolCallId}):\n${summary}
433
- <agent_instruction>Do NOT repeat the question content as text — the user answers inside the widget, which sends the answer to Wixel itself. This is NOT a turn-taking situation: the user's widget answer and the generated result arrive ONLY as events to YOU via list_wixel_events — nothing wakes you if you stop, and ending your turn BREAKS the flow (the user's answer would go nowhere and the result would never be delivered).
434
- Therefore: briefly tell the user to answer in the widget, then keep calling list_wixel_events (handle: "${args.handle}", metaSiteId: "${args.metaSiteId}", waitSeconds: 25) in a continuous loop — the server advances the cursor for you — and do NOT end your turn until a "reply" or "failed" event arrives, however long it takes (the user may think for a while, then generation takes minutes).
435
- - Empty polls that flag questionId ${pending.toolCallId} mean the user has not answered THIS question yet — do NOT call get_wixel_pending_question again for it, just keep polling.
436
- - When a reply/failed event arrives: if the response flags a NEW interactive question, call get_wixel_pending_question again (it renders the next widget). Otherwise present the reply to the user as a normal chat message, always including the "Edit in Wixel" link when provided.</agent_instruction>`
447
+ <agent_instruction>Do NOT repeat the question content as text — the user answers inside the widget, which sends the answer to Wixel itself. This is NOT a turn-taking situation: the result of the user's widget answer reaches YOU only through get_wixel_task — nothing wakes you if you stop, and ending your turn BREAKS the flow (the result would never be delivered).
448
+ Therefore: briefly tell the user to answer in the widget, then keep calling get_wixel_task (taskId: "${args.taskId}", metaSiteId: "${args.metaSiteId}", waitSeconds: 25) in a continuous loop, and do NOT end your turn until it reports the turn done, however long it takes (the user may think for a while, then generation takes minutes). If it names a newer taskId (the widget's answer opened a new turn), follow that one.
449
+ - Polls that flag questionId ${pending.toolCallId} mean the user has not answered THIS question yet — do NOT call get_wixel_pending_question again for it, just keep polling.
450
+ - When the turn is done: if the response flags a NEW interactive question, call get_wixel_pending_question again (it renders the next widget). Otherwise present the reply to the user as a normal chat message, always including the "Edit in Wixel" link when provided.</agent_instruction>`
437
451
  : `The Wixel agent asked:\n${summary}
438
- <agent_instruction>Present the question(s) with their options/content to the user, then send their answer as plain text via send_wixel_message (same handle/metaSiteId) and resume polling list_wixel_events.</agent_instruction>`;
452
+ <agent_instruction>Present the question(s) with their options/content to the user, then send their answer as plain text via send_wixel_message (contextId: "${args.contextId}", metaSiteId: "${args.metaSiteId}") and poll get_wixel_task for the taskId it returns.</agent_instruction>`;
439
453
  return {
440
454
  isError: false,
441
455
  content: [{ type: 'text', text: responseText }],
442
456
  structuredContent: {
443
457
  success: true,
444
- handle: args.handle,
458
+ contextId: args.contextId,
459
+ taskId: args.taskId,
445
460
  metaSiteId: args.metaSiteId,
446
461
  pendingQuestion: pending
447
462
  },
@@ -449,6 +464,9 @@ Therefore: briefly tell the user to answer in the widget, then keep calling list
449
464
  };
450
465
  }
451
466
  catch (error) {
467
+ if (error instanceof WixelA2AError) {
468
+ return a2aErrorResult('Could not read the Wixel task', error);
469
+ }
452
470
  rethrowIfUnauthorized(error);
453
471
  console.error('❌ Error getting Wixel pending question:', error);
454
472
  const errorMessage = error.message;
@@ -460,127 +478,36 @@ Therefore: briefly tell the user to answer in the widget, then keep calling list
460
478
  }
461
479
  });
462
480
  }
463
- export function addStartWixelSessionTool(server, authStrategy, options = {}) {
464
- const { getAccountAuthHeaders, getSiteAuthHeaders } = authStrategy;
465
- registerAppTool(server, 'start_wixel_session', {
466
- title: 'Start Wixel session',
467
- description: `Start (or resume) a session with Wixel — Wix's AI creative studio that GENERATES visual assets from text prompts: images, logos, slide decks / presentations, videos, documents, and event invitations. Use this flow for any "create/generate an image / logo / slides / video" request. It is NOT a website builder.
468
- Call this ONCE per conversation and persist the returned handle + metaSiteId (Wixel's project identifiers) — they are required by send_wixel_message and list_wixel_events.
469
- Without arguments Wixel provisions a new project and session. Pass metaSiteId to reuse an existing project; pass sessionId + metaSiteId to resume a session created by this API.`,
470
- inputSchema: {
471
- sessionId: z
472
- .string()
473
- .optional()
474
- .describe('Existing session ID to resume (requires metaSiteId as well)'),
475
- metaSiteId: z
476
- .string()
477
- .optional()
478
- .describe('Existing meta site ID to reuse its project'),
479
- externalId: z
480
- .string()
481
- .optional()
482
- .describe('Stable identifier of the caller conversation (for diagnostics)'),
483
- workspace: z
484
- .enum(['UNKNOWN', 'BRAND'])
485
- .optional()
486
- .describe('Workspace to provision; omit for the default')
487
- },
488
- annotations: {
489
- readOnlyHint: false,
490
- destructiveHint: false,
491
- openWorldHint: false
492
- },
493
- _meta: TOOL_META
494
- }, async (args, extra) => {
495
- // Both are absent on a fresh session; refined again after the call.
496
- setBi(extra, {
497
- ...(args.metaSiteId ? { siteId: args.metaSiteId } : {}),
498
- ...(args.sessionId ? { conversationId: args.sessionId } : {})
499
- });
500
- if (args.sessionId && !args.metaSiteId) {
501
- return {
502
- isError: true,
503
- content: [
504
- {
505
- type: 'text',
506
- text: 'Invalid input: resuming a session (sessionId) also requires the metaSiteId returned by the original start_wixel_session call.'
507
- }
508
- ],
509
- structuredContent: {
510
- success: false,
511
- error: 'sessionId requires metaSiteId'
512
- }
513
- };
514
- }
515
- const authHeaders = args.metaSiteId
516
- ? await getSiteAuthHeaders(args.metaSiteId)
517
- : await getAccountAuthHeaders();
518
- try {
519
- const session = await startWixelSession({
520
- authHeaders,
521
- conductorOverride: options.conductorOverride,
522
- sessionId: args.sessionId,
523
- metaSiteId: args.metaSiteId,
524
- externalId: args.externalId,
525
- workspace: args.workspace
526
- });
527
- setBi(extra, {
528
- siteId: session.metaSiteId ?? args.metaSiteId,
529
- conversationId: session.handle ?? args.sessionId
530
- });
531
- console.log('[WixelResponseDump]', JSON.stringify({ tool: 'start_wixel_session', args, session }));
532
- return {
533
- isError: false,
534
- content: [
535
- {
536
- type: 'text',
537
- text: `Wixel session ${session.status ?? 'started'} — handle: ${session.handle}, metaSiteId: ${session.metaSiteId}.
538
- <agent_instruction>Persist handle and metaSiteId for this conversation. Send prompts with send_wixel_message, then poll list_wixel_events (advancing the cursor) until a "reply" or "failed" event arrives for the returned turnId.</agent_instruction>`
539
- }
540
- ],
541
- structuredContent: {
542
- success: true,
543
- handle: session.handle,
544
- sessionId: session.sessionId,
545
- metaSiteId: session.metaSiteId,
546
- status: session.status,
547
- expiresAt: session.expiresAt ?? null
548
- }
549
- };
550
- }
551
- catch (error) {
552
- rethrowIfUnauthorized(error);
553
- console.error('❌ Error starting Wixel session:', error);
554
- const errorMessage = error.message;
555
- return {
556
- isError: true,
557
- content: [{ type: 'text', text: errorMessage }],
558
- structuredContent: { success: false, error: errorMessage }
559
- };
560
- }
561
- });
562
- }
563
481
  export function addSendWixelMessageTool(server, authStrategy, options = {}) {
564
482
  const liveCards = options.liveCards ?? createLiveCardRegistry();
565
- const { getSiteAuthHeaders } = authStrategy;
483
+ const { getAccountAuthHeaders, getSiteAuthHeaders } = authStrategy;
566
484
  registerAppTool(server, 'send_wixel_message', {
567
485
  title: 'Send Wixel message',
568
- description: `Send a generation or refinement prompt to a Wixel session (started with start_wixel_session) — e.g. "create an image of a pirate dog", "a 10-slide pitch deck for my bakery", "make the logo blue". The turn runs asynchronously — a successful response only means the turn was ACCEPTED and returns a turnId.
569
- Obtain the agent's answer (including links to the generated assets) by polling list_wixel_events until a "reply" or "failed" event for that turnId appears.
486
+ description: `Send a request to Wixel — Wix's AI creative studio that GENERATES visual assets from text prompts: images, logos, slide decks / presentations, videos, documents, and event invitations. Use it for any "create/generate an image / logo / slides / video" request, and for refinements ("make the logo blue"). It is NOT a website builder.
487
+ The first call starts a conversation: omit contextId (pass metaSiteId only to reuse an existing Wixel project). It returns contextId, taskId and metaSiteId — persist contextId and pass only contextId on every later message of the conversation (it is already bound to its site).
488
+ The turn runs asynchronously — the response only means it was ACCEPTED. Obtain the agent's answer (including links to the generated assets) with get_wixel_task for the returned taskId.
570
489
  Send the user's request as THEY phrased it. Do NOT add creative direction they did not give — style, palette, mood, tone, audience, aspect ratio, slide count or slide topics. Wixel asks the user about those itself, and widget-capable clients render the question as an interactive widget (get_wixel_pending_question), so details you invent silently overwrite the choices the user was about to make. Rephrasing for clarity or translating is fine; filling in the blanks is not.`,
571
490
  inputSchema: {
572
- handle: z.string().describe('Session handle from start_wixel_session'),
573
- metaSiteId: z
574
- .string()
575
- .describe('Meta site ID from start_wixel_session'),
576
491
  text: z
577
492
  .string()
578
- .max(32000)
493
+ .min(1)
494
+ .max(WIXEL_MESSAGE_MAX_LENGTH)
579
495
  .describe("The user's request in their own words — do not add style, palette, audience or content details they did not give"),
580
- transactionId: z
496
+ contextId: z
497
+ .string()
498
+ .min(1)
499
+ .max(512)
500
+ .optional()
501
+ .describe('The conversation, as a previous send_wixel_message returned it. Omit to start a new conversation.'),
502
+ metaSiteId: z
503
+ .string()
504
+ .optional()
505
+ .describe('First message only: an existing meta site ID whose Wixel project the new conversation reuses. Ignored with contextId.'),
506
+ externalId: z
581
507
  .string()
508
+ .max(256)
582
509
  .optional()
583
- .describe('Optional caller correlation ID echoed in terminal events')
510
+ .describe('Stable identifier of the caller conversation (for diagnostics)')
584
511
  },
585
512
  annotations: {
586
513
  readOnlyHint: false,
@@ -589,72 +516,74 @@ Send the user's request as THEY phrased it. Do NOT add creative direction they d
589
516
  },
590
517
  _meta: TOOL_META
591
518
  }, async (args, extra) => {
592
- setBi(extra, {
593
- siteId: args.metaSiteId,
594
- conversationId: args.handle
595
- });
596
- const authHeaders = await getSiteAuthHeaders(args.metaSiteId);
519
+ // A later message names only its conversation: the agent reads the site
520
+ // from the contextId and checks it against the caller's own sites, so
521
+ // the account identity is enough. A site identity is only needed when a
522
+ // first message reuses an existing site's project.
523
+ const reuseSite = args.contextId ? undefined : args.metaSiteId;
524
+ setBi(extra, reuseSite ? { siteId: reuseSite } : {});
525
+ const authHeaders = reuseSite
526
+ ? await getSiteAuthHeaders(reuseSite)
527
+ : await getAccountAuthHeaders();
597
528
  try {
598
529
  // The user's text goes through as typed — nothing is appended. An
599
530
  // earlier hint here asked Wixel to stop and ask about style/palette
600
531
  // whenever a request left them open, which is why MCP users got
601
532
  // questions the Wixel app never asks. Wixel's own flows (a deck's
602
533
  // outline and style review) still ask, and the live card renders them.
603
- const text = args.text;
604
- const result = await sendWixelSessionMessage({
534
+ const task = await sendWixelMessage({
605
535
  authHeaders,
606
536
  conductorOverride: options.conductorOverride,
607
- handle: args.handle,
608
- metaSiteId: args.metaSiteId,
609
- text,
610
- transactionId: args.transactionId
537
+ text: args.text,
538
+ contextId: args.contextId,
539
+ metaSiteId: reuseSite,
540
+ externalId: args.externalId
541
+ });
542
+ const summary = summarizeTask(task);
543
+ const metaSiteId = summary.metaSiteId ?? reuseSite ?? null;
544
+ setBi(extra, {
545
+ ...(metaSiteId ? { siteId: metaSiteId } : {}),
546
+ ...(summary.sessionId ? { conversationId: summary.sessionId } : {})
611
547
  });
612
548
  console.log('[WixelResponseDump]', JSON.stringify({
613
549
  tool: 'send_wixel_message',
614
- args: { ...args, text: text.slice(0, 400) },
615
- result
550
+ args: { ...args, text: args.text.slice(0, 400) },
551
+ task: {
552
+ id: task.id,
553
+ contextId: task.contextId,
554
+ state: summary.state,
555
+ metadata: task.metadata
556
+ }
616
557
  }));
617
- if (!result.ok) {
618
- return {
619
- isError: true,
620
- content: [
621
- {
622
- type: 'text',
623
- text: `Wixel rejected the message: ${result.error ?? 'unknown error'} (${result.code ?? 'no code'})`
624
- }
625
- ],
626
- structuredContent: {
627
- success: false,
628
- error: result.error,
629
- code: result.code
630
- }
631
- };
632
- }
633
- if (result.turnId) {
634
- liveCards.forHandle(args.handle).lastSentTurn = result.turnId;
635
- }
558
+ liveCards.forContext(task.contextId).lastSentTask = task.id;
559
+ const steeredNote = summary.steered
560
+ ? "\nA turn was already running, so the message was added to it: this is that turn's task."
561
+ : '';
636
562
  // Widget clients get a live result card that follows the turn itself
637
563
  // (progress, questions, final asset), so the agent renders it once
638
564
  // and stops — no polling loop of tool calls in the conversation.
639
565
  const instruction = options.supportsWidgets?.()
640
- ? `<agent_instruction>Now call get_wixel_asset_result with handle: "${args.handle}", metaSiteId: "${args.metaSiteId}", turnId: "${result.turnId}", prompt: <the user's request as they phrased it>. It renders a live result card that shows progress, asks the user any question Wixel has, and displays the final asset. Then end your turn — do NOT poll list_wixel_events and do NOT call get_wixel_pending_question, and do NOT write a message after the card (it explains itself).</agent_instruction>`
641
- : `<agent_instruction>Poll list_wixel_events (handle: "${args.handle}", metaSiteId: "${args.metaSiteId}", waitSeconds: 25, advancing the returned cursor) until an event with turnId "${result.turnId}" has type "reply" or "failed". Turns can take minutes for generation tasks — keep polling.
642
- After a reply that asks the user something (review an outline, pick a style, confirm), call get_wixel_pending_question (same handle/metaSiteId) and follow its response instruction.</agent_instruction>`;
566
+ ? `<agent_instruction>Now call get_wixel_asset_result with contextId: "${task.contextId}", taskId: "${task.id}", metaSiteId: "${metaSiteId}", prompt: <the user's request as they phrased it>. It renders a live result card that shows progress, asks the user any question Wixel has, and displays the final asset. Then end your turn — do NOT poll get_wixel_task and do NOT call get_wixel_pending_question, and do NOT write a message after the card (it explains itself).</agent_instruction>`
567
+ : `<agent_instruction>Poll get_wixel_task (taskId: "${task.id}", metaSiteId: "${metaSiteId}", waitSeconds: 25) until it reports the turn done. Turns can take minutes for generation tasks — keep polling.
568
+ If it flags an interactive question, call get_wixel_pending_question (contextId: "${task.contextId}", taskId: "${task.id}", metaSiteId: "${metaSiteId}") and follow its response instruction.</agent_instruction>`;
643
569
  const structuredContent = {
644
570
  success: true,
645
- sessionId: result.sessionId,
646
- turnId: result.turnId,
647
- transactionId: result.transactionId ?? null
571
+ contextId: task.contextId,
572
+ taskId: task.id,
573
+ metaSiteId,
574
+ sessionId: summary.sessionId,
575
+ state: summary.state,
576
+ steered: summary.steered
648
577
  };
649
578
  return {
650
579
  isError: false,
651
580
  content: [
652
581
  {
653
582
  type: 'text',
654
- text: `Turn accepted — turnId: ${result.turnId}.
583
+ text: `Turn accepted — contextId: ${task.contextId}, taskId: ${task.id}, metaSiteId: ${metaSiteId}.${steeredNote}
655
584
  ${instruction}`
656
585
  },
657
- // The widgets send answers through this tool and need the turnId
586
+ // The widgets send answers through this tool and need the taskId
658
587
  // back; hosts that drop structuredContent get it from this item.
659
588
  { type: 'text', text: JSON.stringify(structuredContent) }
660
589
  ],
@@ -662,6 +591,9 @@ ${instruction}`
662
591
  };
663
592
  }
664
593
  catch (error) {
594
+ if (error instanceof WixelA2AError) {
595
+ return a2aErrorResult('Wixel rejected the message', error);
596
+ }
665
597
  rethrowIfUnauthorized(error);
666
598
  console.error('❌ Error sending Wixel message:', error);
667
599
  const errorMessage = error.message;
@@ -673,37 +605,24 @@ ${instruction}`
673
605
  }
674
606
  });
675
607
  }
676
- export function addListWixelEventsTool(server, authStrategy, options = {}) {
608
+ const LONG_POLL_INTERVAL_MS = 2500;
609
+ const LONG_POLL_MAX_SECONDS = 25;
610
+ export function addGetWixelTaskTool(server, authStrategy, options = {}) {
677
611
  const liveCards = options.liveCards ?? createLiveCardRegistry();
678
612
  const { getSiteAuthHeaders } = authStrategy;
679
- // Models routinely forget to thread the cursor through poll loops, which
680
- // makes them re-read old replies and misjudge the session state. Remember
681
- // the last served cursor per handle (per MCP session) and use it whenever
682
- // "since" is omitted.
683
- const lastCursorByHandle = new Map();
684
- registerAppTool(server, 'list_wixel_events', {
685
- title: 'List Wixel events',
686
- description: `Read durable turn events from a Wixel Agent session inbox. Pass the cursor returned by the previous call as "since" to only receive newer events.
687
- Event types: "reply" (the agent's answer), "failed" (the turn failed), and "user" (only with historyMode FULL). Poll until the awaited turnId gets a reply/failed event.`,
613
+ registerAppTool(server, 'get_wixel_task', {
614
+ title: 'Get Wixel task',
615
+ description: `Report a Wixel turn (a task returned by send_wixel_message): still working, parked on a question for the user, done with the agent's reply and the generated assets, or failed.
616
+ Poll it with waitSeconds: 25 until it reports the turn done.`,
688
617
  inputSchema: {
689
- handle: z.string().describe('Session handle from start_wixel_session'),
690
- metaSiteId: z
691
- .string()
692
- .describe('Meta site ID from start_wixel_session'),
693
- since: z
694
- .string()
695
- .optional()
696
- .describe('Cursor returned by the previous list_wixel_events call. May be omitted — the server remembers the last cursor it served for this handle and continues from there.'),
697
- historyMode: z
698
- .enum(['REPLIES_ONLY', 'FULL'])
699
- .optional()
700
- .describe('REPLIES_ONLY (default) returns terminal events only; FULL also includes echoed user messages'),
618
+ taskId: taskIdInput,
619
+ metaSiteId: metaSiteIdInput,
701
620
  waitSeconds: z
702
621
  .number()
703
622
  .min(0)
704
- .max(25)
623
+ .max(LONG_POLL_MAX_SECONDS)
705
624
  .optional()
706
- .describe('Long-poll: when no new events are available, hold the request up to this many seconds until one arrives. Recommended: 25 while waiting on generation or on a widget answer.')
625
+ .describe('Long-poll: while the turn is still working, hold the request up to this many seconds until it ends. Recommended: 25 while waiting on generation or on a widget answer.')
707
626
  },
708
627
  annotations: {
709
628
  readOnlyHint: true,
@@ -712,39 +631,38 @@ Event types: "reply" (the agent's answer), "failed" (the turn failed), and "user
712
631
  },
713
632
  _meta: TOOL_META
714
633
  }, async (args, extra) => {
715
- setBi(extra, {
716
- siteId: args.metaSiteId,
717
- conversationId: args.handle
718
- });
634
+ setBi(extra, { siteId: args.metaSiteId });
719
635
  const authHeaders = await getSiteAuthHeaders(args.metaSiteId);
636
+ const base = {
637
+ authHeaders,
638
+ conductorOverride: options.conductorOverride
639
+ };
720
640
  try {
721
- const since = args.since ?? lastCursorByHandle.get(args.handle);
722
- const fetchEvents = () => listWixelSessionEvents({
723
- authHeaders,
724
- conductorOverride: options.conductorOverride,
725
- handle: args.handle,
726
- metaSiteId: args.metaSiteId,
727
- since,
728
- historyMode: args.historyMode
641
+ const fetchTask = () => getSiteTask({
642
+ ...base,
643
+ taskId: args.taskId,
644
+ metaSiteId: args.metaSiteId
729
645
  });
730
- // A live result card is following the last turn: the agent has
731
- // nothing to do here, and text instructions alone don't stop its
732
- // polling loop — so refuse the call (no fetch, cursor untouched).
733
- // The tool works again once the turn has ended (the card saw it, or
734
- // the inbox says so when the card is gone).
646
+ let task = await fetchTask();
647
+ let summary = summarizeTask(task);
648
+ setBi(extra, {
649
+ siteId: args.metaSiteId,
650
+ ...(summary.sessionId ? { conversationId: summary.sessionId } : {})
651
+ });
652
+ // A live result card is following the conversation's latest turn:
653
+ // the agent has nothing to do here, and text instructions alone don't
654
+ // stop its polling loop — so refuse the call. The tool works again
655
+ // once that turn has ended (the card saw it, or the task says so
656
+ // when the card is gone).
735
657
  if (options.supportsWidgets?.() === true &&
736
- (await liveCards.isActive(args.handle, {
737
- authHeaders,
738
- conductorOverride: options.conductorOverride,
739
- metaSiteId: args.metaSiteId
740
- }))) {
741
- console.log('[WixelResponseDump]', JSON.stringify({ tool: 'list_wixel_events', args, refused: true }));
658
+ (await liveCards.isActive(task.contextId, terminalCheck(base, task)))) {
659
+ console.log('[WixelResponseDump]', JSON.stringify({ tool: 'get_wixel_task', args, refused: true }));
742
660
  return {
743
661
  isError: true,
744
662
  content: [
745
663
  {
746
664
  type: 'text',
747
- text: `Not needed: the live result card is following this generation and will display progress, any question, and the final result by itself. This is not a failure — there is nothing to wait for here. Do NOT call list_wixel_events, get_wixel_pending_question or get_wixel_asset_result again for this generation. End your turn now WITHOUT writing any message — no summary, no "Wixel is working on it", nothing: the card already says it, and a sentence under it is redundant. If your client forces a reply, use at most two words.`
665
+ text: `Not needed: the live result card is following this generation and will display progress, any question, and the final result by itself. This is not a failure — there is nothing to wait for here. Do NOT call get_wixel_task, get_wixel_pending_question or get_wixel_asset_result again for this generation. ${END_TURN_SILENTLY}`
748
666
  }
749
667
  ],
750
668
  structuredContent: {
@@ -754,181 +672,41 @@ Event types: "reply" (the agent's answer), "failed" (the turn failed), and "user
754
672
  }
755
673
  };
756
674
  }
757
- let result = await fetchEvents();
758
- // Long-poll: hold the request while the inbox is empty so callers can
675
+ // Long-poll: hold the request while the turn works so callers can
759
676
  // wait on slow turns (generation, widget answers) without hammering.
760
- const waitDeadline = Date.now() + Math.min(Math.max(args.waitSeconds ?? 0, 0), 25) * 1000;
761
- while (!result.error &&
762
- (result.events ?? []).length === 0 &&
763
- Date.now() + 2500 <= waitDeadline) {
764
- await new Promise((resolve) => setTimeout(resolve, 2500));
765
- result = await fetchEvents();
766
- }
767
- console.log('[WixelResponseDump]', JSON.stringify({ tool: 'list_wixel_events', args, result }));
768
- if (result.error) {
769
- return {
770
- isError: true,
771
- content: [
772
- {
773
- type: 'text',
774
- text: `Failed to list events: ${result.error} (${result.code ?? 'no code'})`
775
- }
776
- ],
777
- structuredContent: {
778
- success: false,
779
- error: result.error,
780
- code: result.code
781
- }
782
- };
783
- }
784
- const events = result.events ?? [];
785
- if (result.cursor) {
786
- lastCursorByHandle.set(args.handle, result.cursor);
677
+ const waitDeadline = Date.now() +
678
+ Math.min(Math.max(args.waitSeconds ?? 0, 0), LONG_POLL_MAX_SECONDS) *
679
+ 1000;
680
+ while (!summary.done &&
681
+ Date.now() + LONG_POLL_INTERVAL_MS <= waitDeadline) {
682
+ await new Promise((resolve) => setTimeout(resolve, LONG_POLL_INTERVAL_MS));
683
+ task = await fetchTask();
684
+ summary = summarizeTask(task);
787
685
  }
788
- // Check whether the turn is parked on an interactive question
789
- // (ask_user is intercepted on external sessions but its structured
790
- // payload is persisted) so clients render it as a widget. This must
791
- // also run on EMPTY polls: custom_widget questions park the turn
792
- // without emitting a reply event, so the inbox stays empty forever.
793
- let questionInstruction = '';
794
- let pendingQuestion = null;
795
- let turnInProgress = false;
796
- let editorUrl = null;
797
- // A reply that references created assets IS the deliverable — the
798
- // flow is complete, so any parked follow-up question (e.g. "pick a
799
- // new style?") must not spawn another widget.
800
- let deliveredAssets = false;
801
- let deliveredAssetIds = [];
802
- let deliveredMediaUrls = [];
803
- const hasReply = events.some((event) => event.type === 'reply');
804
- if (hasReply) {
805
- // Resolve an "edit in Wixel" link once the reply references created
806
- // assets: the session is bound to its Wixel project, which is the
807
- // editor deep-link target. Some replies carry asset-id markers,
808
- // others just embed the media URL — both count as delivered.
809
- const replyTexts = events
810
- .filter((event) => event.type === 'reply' && event.text)
811
- .map((event) => event.text);
812
- deliveredAssetIds = replyTexts.flatMap(extractAssetIds);
813
- // Same rule as the live card: a reply that asks the user something
814
- // (e.g. an animation plan linking its source image) delivers
815
- // nothing by its media URLs alone.
816
- deliveredMediaUrls = replyTexts
817
- .filter((text) => !isQuestionReply(text))
818
- .flatMap(extractMediaUrls);
819
- if (deliveredAssetIds.length || deliveredMediaUrls.length) {
820
- deliveredAssets = true;
821
- // Delivered by a turn a live card follows: the card shows them,
822
- // so a get_wixel_asset_result for these ids is a duplicate. Only
823
- // that turn's ids — a batch may also carry a turn no card follows.
824
- const record = liveCards.get(args.handle);
825
- if (record) {
826
- events
827
- .filter((event) => event.type === 'reply' &&
828
- !!event.text &&
829
- !!event.turnId &&
830
- record.turns.has(event.turnId))
831
- .flatMap((event) => [
832
- ...extractAssetIds(event.text).slice(0, MAX_RESULT_ITEMS),
833
- ...extractMediaUrls(event.text).slice(0, MAX_RESULT_ITEMS)
834
- ])
835
- .forEach((item) => record.shownAssets.add(item));
836
- }
837
- try {
838
- const projectId = await getWixelSessionProjectId({
839
- authHeaders,
840
- conductorOverride: options.conductorOverride,
841
- sessionId: args.handle
842
- });
843
- if (projectId) {
844
- editorUrl = buildWixelEditorUrl(args.metaSiteId, projectId);
845
- }
846
- }
847
- catch (projectError) {
848
- console.warn('[Wixel] editor link resolution failed:', projectError.message);
849
- }
850
- }
851
- }
852
- if (hasReply || events.length === 0) {
853
- try {
854
- if (hasReply) {
855
- if (!deliveredAssets) {
856
- pendingQuestion = await getWixelPendingQuestion({
857
- authHeaders,
858
- conductorOverride: options.conductorOverride,
859
- sessionId: args.handle
860
- });
861
- }
862
- }
863
- else {
864
- // On an empty poll only a parked ask_user counts — a question
865
- // already handled (policy fallback or answered in the Wixel UI)
866
- // must not resurface while the turn keeps generating. The same
867
- // entries read also tells whether a turn is actively running,
868
- // so the caller knows to keep polling rather than turn to the
869
- // user.
870
- const activity = await getWixelSessionActivity({
871
- authHeaders,
872
- conductorOverride: options.conductorOverride,
873
- sessionId: args.handle
874
- });
875
- pendingQuestion = activity.parkedQuestion;
876
- turnInProgress = activity.turnInProgress;
877
- }
878
- }
879
- catch (questionError) {
880
- console.warn('[Wixel] pending question fetch failed:', questionError.message);
881
- }
882
- if (pendingQuestion) {
883
- questionInstruction = `
884
- IMPORTANT: the agent is asking the user an interactive question (${pendingQuestion.questions
885
- .map((question) => question.kind)
886
- .join(', ')}; questionId: ${pendingQuestion.toolCallId}). If you have NOT yet displayed this questionId, you MUST now call get_wixel_pending_question (handle: "${args.handle}", metaSiteId: "${args.metaSiteId}", questionId: "${pendingQuestion.toolCallId}") and follow its response instruction BEFORE responding to the user — do not present the question yourself. If you already displayed this questionId, the user simply hasn't answered it yet — do NOT end your turn (the answer arrives only as an event to you; stopping breaks the flow): keep polling list_wixel_events (waitSeconds: 25) without re-calling get_wixel_pending_question.`;
887
- }
888
- }
889
- const summary = events.length
890
- ? events
891
- .map((event) => `[${event.type}] turnId: ${event.turnId ?? '-'}${event.text ? ` — ${event.text}` : ''}`)
892
- .join('\n')
893
- : pendingQuestion
894
- ? 'The turn is parked on an interactive question awaiting the user.'
895
- : turnInProgress
896
- ? 'No new events yet — Wixel is actively working on the current turn.'
897
- : 'No new events yet — call list_wixel_events again with the same cursor.';
898
- // An empty poll while a turn is generating must not read as "waiting
899
- // on the user" — the caller's next step is to keep polling, nothing
900
- // else.
901
- const workingInstruction = !events.length && !pendingQuestion && turnInProgress
902
- ? `
903
- <agent_instruction>Wixel is actively generating — the user does NOT need to do anything. Do NOT end your turn and do NOT ask the user for input: call list_wixel_events again (handle: "${args.handle}", metaSiteId: "${args.metaSiteId}", same cursor, waitSeconds: 25) and repeat until a "reply" or "failed" event arrives, however many polls it takes.</agent_instruction>`
904
- : '';
905
- const editorLine = deliveredAssets
906
- ? `${editorUrl ? `\nEdit in Wixel: ${editorUrl}` : ''}\n<agent_instruction>The asset is READY — this reply is the final deliverable. You MUST now call get_wixel_asset_result with EXACTLY these arguments: handle: "${args.handle}", metaSiteId: "${args.metaSiteId}", assetIds: ${JSON.stringify(deliveredAssetIds)}, mediaUrls: ${JSON.stringify(deliveredMediaUrls)}, prompt: <the user's original request>. In widget-capable clients it renders the result card (preview + edit/download actions); otherwise present the reply as text${editorUrl ? ' and ALWAYS include the Edit in Wixel link' : ''}. Do NOT call get_wixel_pending_question now — if the reply offers optional follow-ups (restyle, edits), mention them as text.</agent_instruction>`
907
- : '';
908
- return {
909
- isError: false,
910
- content: [
911
- {
912
- type: 'text',
913
- text: `${summary}\ncursor: ${result.cursor}${editorLine}${questionInstruction}${workingInstruction}`
914
- }
915
- ],
916
- structuredContent: {
917
- success: true,
918
- events,
919
- cursor: result.cursor,
920
- hasPendingQuestion: !!pendingQuestion,
921
- pendingQuestionId: pendingQuestion?.toolCallId ?? null,
922
- turnInProgress,
923
- editorUrl,
924
- assetIds: deliveredAssetIds,
925
- mediaUrls: deliveredMediaUrls
926
- }
927
- };
686
+ const result = await describeTask({
687
+ base,
688
+ task,
689
+ summary,
690
+ liveCards,
691
+ metaSiteId: args.metaSiteId
692
+ });
693
+ console.log('[WixelResponseDump]', JSON.stringify({
694
+ tool: 'get_wixel_task',
695
+ args,
696
+ state: summary.state,
697
+ replyText: summary.replyText,
698
+ assetIds: summary.assetIds,
699
+ mediaUrls: summary.mediaUrls,
700
+ pendingQuestionId: result.structuredContent.pendingQuestionId
701
+ }).slice(0, 30000));
702
+ return result;
928
703
  }
929
704
  catch (error) {
705
+ if (error instanceof WixelA2AError) {
706
+ return a2aErrorResult('Could not read the Wixel task', error);
707
+ }
930
708
  rethrowIfUnauthorized(error);
931
- console.error('❌ Error listing Wixel events:', error);
709
+ console.error('❌ Error getting Wixel task:', error);
932
710
  const errorMessage = error.message;
933
711
  return {
934
712
  isError: true,
@@ -938,6 +716,125 @@ IMPORTANT: the agent is asking the user an interactive question (${pendingQuesti
938
716
  }
939
717
  });
940
718
  }
719
+ /** get_wixel_task's answer for a task it has read. */
720
+ async function describeTask({ base, task, summary, liveCards, metaSiteId }) {
721
+ const { taskId, contextId, sessionId } = summary;
722
+ const ids = `contextId: "${contextId}", taskId: "${taskId}", metaSiteId: "${metaSiteId}"`;
723
+ const failed = summary.done && summary.state !== 'TASK_STATE_COMPLETED';
724
+ // A reply that names assets or links media IS the deliverable — the flow
725
+ // is complete, so any follow-up question (e.g. "pick a new style?") must
726
+ // not spawn another widget.
727
+ const delivered = summary.state === 'TASK_STATE_COMPLETED' &&
728
+ (summary.assetIds.length > 0 || summary.mediaUrls.length > 0);
729
+ let editorUrl = null;
730
+ if (delivered) {
731
+ // Delivered by a turn a live card follows: the card shows them, so a
732
+ // get_wixel_asset_result for these ids is a duplicate.
733
+ const record = liveCards.get(task.contextId);
734
+ if (record?.tasks.has(task.id)) {
735
+ [
736
+ ...summary.assetIds.slice(0, MAX_RESULT_ITEMS),
737
+ ...summary.mediaUrls.slice(0, MAX_RESULT_ITEMS)
738
+ ].forEach((item) => record.shownAssets.add(item));
739
+ }
740
+ // The session is bound to its Wixel project, which is the editor
741
+ // deep-link target.
742
+ try {
743
+ const projectId = sessionId
744
+ ? await getWixelSessionProjectId({ ...base, sessionId })
745
+ : null;
746
+ if (projectId)
747
+ editorUrl = buildWixelEditorUrl(metaSiteId, projectId);
748
+ }
749
+ catch (projectError) {
750
+ console.warn('[Wixel] editor link resolution failed:', projectError.message);
751
+ }
752
+ }
753
+ // A question worth surfacing: on a working turn only a parked ask_user
754
+ // counts (a question already handled — policy fallback or answered in the
755
+ // Wixel UI — must not resurface while the turn keeps generating); on a
756
+ // completed turn without a delivery, the question the reply restates.
757
+ let pendingQuestion = null;
758
+ let turnInProgress = false;
759
+ if (sessionId && !failed && !delivered) {
760
+ try {
761
+ if (summary.done) {
762
+ pendingQuestion = await getWixelPendingQuestion({
763
+ ...base,
764
+ sessionId,
765
+ turnId: summary.turnId
766
+ });
767
+ }
768
+ else {
769
+ const activity = await getWixelSessionActivity({ ...base, sessionId });
770
+ pendingQuestion = activity.parkedQuestion;
771
+ turnInProgress = activity.turnInProgress;
772
+ }
773
+ }
774
+ catch (questionError) {
775
+ console.warn('[Wixel] pending question fetch failed:', questionError.message);
776
+ }
777
+ }
778
+ // Not yet picked up, or between steps: still the turn's own work.
779
+ if (!summary.done && !pendingQuestion)
780
+ turnInProgress = true;
781
+ // The widget's answer to a question opens its own turn when the question's
782
+ // turn had already ended; the agent only knows the earlier task.
783
+ const latest = liveCards.get(task.contextId)?.lastSentTask ?? null;
784
+ const newerTaskId = summary.done && latest && latest !== task.id ? latest : null;
785
+ const lines = [];
786
+ if (failed) {
787
+ lines.push(`The turn ended without a result (${summary.state})${summary.replyText ? `: ${summary.replyText}` : '.'}`);
788
+ }
789
+ else if (summary.done) {
790
+ lines.push(summary.replyText
791
+ ? `Wixel replied: ${summary.replyText}`
792
+ : 'The turn is done.');
793
+ }
794
+ else if (pendingQuestion) {
795
+ lines.push('The turn is parked on an interactive question awaiting the user.');
796
+ }
797
+ else {
798
+ lines.push('Wixel is actively working on this turn.');
799
+ }
800
+ if (delivered) {
801
+ if (editorUrl)
802
+ lines.push(`Edit in Wixel: ${editorUrl}`);
803
+ lines.push(`<agent_instruction>The asset is READY — this reply is the final deliverable. You MUST now call get_wixel_asset_result with EXACTLY these arguments: ${ids}, assetIds: ${JSON.stringify(summary.assetIds)}, mediaUrls: ${JSON.stringify(summary.mediaUrls)}, prompt: <the user's original request>. In widget-capable clients it renders the result card (preview + edit/download actions); otherwise present the reply as text${editorUrl ? ' and ALWAYS include the Edit in Wixel link' : ''}. Do NOT call get_wixel_pending_question now — if the reply offers optional follow-ups (restyle, edits), mention them as text.</agent_instruction>`);
804
+ }
805
+ else if (pendingQuestion) {
806
+ lines.push(`IMPORTANT: the agent is asking the user an interactive question (${pendingQuestion.questions
807
+ .map((question) => question.kind)
808
+ .join(', ')}; questionId: ${pendingQuestion.toolCallId}). If you have NOT yet displayed this questionId, you MUST now call get_wixel_pending_question (${ids}, questionId: "${pendingQuestion.toolCallId}") and follow its response instruction BEFORE responding to the user — do not present the question yourself. If you already displayed this questionId, the user simply hasn't answered it yet — do NOT end your turn (stopping breaks the flow): keep polling get_wixel_task (waitSeconds: 25) without re-calling get_wixel_pending_question.`);
809
+ }
810
+ else if (newerTaskId) {
811
+ lines.push(`<agent_instruction>A newer turn was sent in this conversation since (taskId: "${newerTaskId}") — the user's answer from the widget. Poll get_wixel_task (taskId: "${newerTaskId}", metaSiteId: "${metaSiteId}", waitSeconds: 25) instead.</agent_instruction>`);
812
+ }
813
+ else if (turnInProgress) {
814
+ // A working turn must not read as "waiting on the user" — the caller's
815
+ // next step is to keep polling, nothing else.
816
+ lines.push(`<agent_instruction>Wixel is actively generating — the user does NOT need to do anything. Do NOT end your turn and do NOT ask the user for input: call get_wixel_task again (taskId: "${taskId}", metaSiteId: "${metaSiteId}", waitSeconds: 25) and repeat until it reports the turn done, however many polls it takes.</agent_instruction>`);
817
+ }
818
+ return {
819
+ isError: false,
820
+ content: [{ type: 'text', text: lines.join('\n') }],
821
+ structuredContent: {
822
+ success: true,
823
+ state: summary.state,
824
+ done: summary.done,
825
+ replyText: summary.replyText,
826
+ assetIds: summary.assetIds,
827
+ mediaUrls: summary.mediaUrls,
828
+ contextId,
829
+ taskId,
830
+ hasPendingQuestion: !!pendingQuestion,
831
+ pendingQuestionId: pendingQuestion?.toolCallId ?? null,
832
+ turnInProgress,
833
+ editorUrl,
834
+ newerTaskId
835
+ }
836
+ };
837
+ }
941
838
  export function addGetWixelStylePresetsTool(server, authStrategy, options = {}) {
942
839
  const { getSiteAuthHeaders } = authStrategy;
943
840
  registerAppTool(server, 'get_wixel_style_presets', {
@@ -995,13 +892,11 @@ export function addGetWixelStylePresetsTool(server, authStrategy, options = {})
995
892
  }
996
893
  // Widget-internal (not exposed to the model). The result card renders the
997
894
  // moment the request is sent and drives the rest of the flow itself: it polls
998
- // this for the answer's turn and gets back "generating", a "question" to show
999
- // inline, the "done" assets, or "failed". The inbox is replayed from cursor
1000
- // "0" with an explicit cursor, so the cursor list_wixel_events remembers per
1001
- // handle for the agent is never advanced. "Done" is the reply/failed event of
1002
- // the turn — the same signal the agent would have waited on; the entries log
1003
- // only decides whether the turn is parked on a question, because the agent
1004
- // emits text mid-turn and that log cannot tell it from the final reply.
895
+ // this for the answer's task and gets back "generating", a "question" to show
896
+ // inline, the "done" assets, or "failed". "Done" is the task's terminal state
897
+ // — the same signal the agent would have waited on with get_wixel_task; the
898
+ // entries log only decides whether the turn is parked on a question, because
899
+ // a parked turn's task stays WORKING.
1005
900
  // `ui.visibility` is what MCP Apps hosts read; ChatGPT reads
1006
901
  // `openai/visibility` instead, so both are set or its model would see the tool.
1007
902
  const WIDGET_ONLY_TOOL_META = {
@@ -1015,17 +910,12 @@ export function addGetWixelTurnResultTool(server, authStrategy, options = {}) {
1015
910
  title: 'Get Wixel turn result',
1016
911
  description: 'Widget-internal: the live result card polls this for the outcome of a turn (generating, a question to answer, the delivered assets, or a failure). Not needed for normal conversation flows.',
1017
912
  inputSchema: {
1018
- handle: z.string().describe('Session handle from start_wixel_session'),
1019
- metaSiteId: z
1020
- .string()
1021
- .describe('Meta site ID from start_wixel_session'),
1022
- turnId: z
1023
- .string()
1024
- .describe('The turn to report on (returned by send_wixel_message)'),
913
+ taskId: taskIdInput,
914
+ metaSiteId: metaSiteIdInput,
1025
915
  live: z
1026
916
  .boolean()
1027
917
  .optional()
1028
- .describe('Set by the live result card: marks the turn as followed by the card, so the agent is told to stand back for it.')
918
+ .describe('Set by the live result card: marks the task as followed by the card, so the agent is told to stand back for it.')
1029
919
  },
1030
920
  annotations: {
1031
921
  readOnlyHint: true,
@@ -1034,53 +924,46 @@ export function addGetWixelTurnResultTool(server, authStrategy, options = {}) {
1034
924
  },
1035
925
  _meta: WIDGET_ONLY_TOOL_META
1036
926
  }, async (args, extra) => {
1037
- setBi(extra, {
1038
- siteId: args.metaSiteId,
1039
- conversationId: args.handle
1040
- });
1041
- // A turn the live card follows — including the turns its inline
1042
- // answers open, which get_wixel_asset_result never saw.
1043
- if (args.live) {
1044
- const record = liveCards.forHandle(args.handle);
1045
- if (!record.turns.has(args.turnId)) {
1046
- record.turns.set(args.turnId, Date.now());
1047
- }
1048
- }
927
+ setBi(extra, { siteId: args.metaSiteId });
1049
928
  const authHeaders = await getSiteAuthHeaders(args.metaSiteId);
1050
929
  const base = {
1051
930
  authHeaders,
1052
931
  conductorOverride: options.conductorOverride
1053
932
  };
1054
933
  try {
1055
- const inbox = await listWixelSessionEvents({
934
+ const task = await getSiteTask({
1056
935
  ...base,
1057
- handle: args.handle,
1058
- metaSiteId: args.metaSiteId,
1059
- since: '0'
936
+ taskId: args.taskId,
937
+ metaSiteId: args.metaSiteId
938
+ });
939
+ const summary = summarizeTask(task);
940
+ const { sessionId } = summary;
941
+ setBi(extra, {
942
+ siteId: args.metaSiteId,
943
+ ...(sessionId ? { conversationId: sessionId } : {})
1060
944
  });
1061
- if (inbox.error) {
1062
- throw new Error(`Failed to replay events: ${inbox.error} (${inbox.code ?? 'no code'})`);
945
+ // A task the live card follows — including the tasks its inline
946
+ // answers open, which get_wixel_asset_result never saw.
947
+ if (args.live) {
948
+ const record = liveCards.forContext(task.contextId);
949
+ if (!record.tasks.has(task.id)) {
950
+ record.tasks.set(task.id, Date.now());
951
+ }
1063
952
  }
1064
- const terminal = (inbox.events ?? []).find((event) => event.turnId === args.turnId &&
1065
- (event.type === 'reply' || event.type === 'failed'));
1066
953
  let status = 'generating';
1067
954
  let assets = [];
1068
955
  let editorUrl = null;
1069
956
  let pendingQuestion = null;
1070
- const replyText = terminal?.text ?? null;
1071
- if (terminal?.type === 'failed') {
957
+ const replyText = summary.done ? summary.replyText : null;
958
+ if (summary.done && summary.state !== 'TASK_STATE_COMPLETED') {
1072
959
  status = 'failed';
1073
960
  }
1074
- else if (terminal) {
961
+ else if (summary.done) {
1075
962
  // Capped like the resolver's input, so what gets recorded as
1076
963
  // shown below is exactly what was considered for rendering.
1077
- const assetIds = extractAssetIds(terminal.text ?? '').slice(0, MAX_RESULT_ITEMS);
1078
- const mediaUrls = extractMediaUrls(terminal.text ?? '').slice(0, MAX_RESULT_ITEMS);
1079
- // Media URLs alone count only when the reply is not a question:
1080
- // an animation plan links the source image and asks for approval.
1081
- const delivers = assetIds.length > 0 ||
1082
- (mediaUrls.length > 0 && !isQuestionReply(terminal.text ?? ''));
1083
- if (delivers) {
964
+ const assetIds = summary.assetIds.slice(0, MAX_RESULT_ITEMS);
965
+ const mediaUrls = summary.mediaUrls.slice(0, MAX_RESULT_ITEMS);
966
+ if (assetIds.length || mediaUrls.length) {
1084
967
  status = 'done';
1085
968
  // Only the live card displays what it resolves. The standalone
1086
969
  // question widget polls for the status alone: resolving for it
@@ -1089,7 +972,7 @@ export function addGetWixelTurnResultTool(server, authStrategy, options = {}) {
1089
972
  if (args.live) {
1090
973
  ({ assets, editorUrl } = await resolveWixelAssetResults({
1091
974
  ...base,
1092
- handle: args.handle,
975
+ sessionId,
1093
976
  metaSiteId: args.metaSiteId,
1094
977
  assetIds,
1095
978
  mediaUrls
@@ -1098,7 +981,7 @@ export function addGetWixelTurnResultTool(server, authStrategy, options = {}) {
1098
981
  // resolution that came back empty (transient lookup failure)
1099
982
  // leaves the result for the agent's get_wixel_asset_result.
1100
983
  if (assets.length) {
1101
- const record = liveCards.forHandle(args.handle);
984
+ const record = liveCards.forContext(task.contextId);
1102
985
  [...assetIds, ...mediaUrls].forEach((item) => record.shownAssets.add(item));
1103
986
  }
1104
987
  }
@@ -1107,19 +990,22 @@ export function addGetWixelTurnResultTool(server, authStrategy, options = {}) {
1107
990
  // A reply without a delivery is either a question restated as
1108
991
  // text (its structured payload is still pending) or a plain
1109
992
  // message — the card shows it with a reply box.
1110
- pendingQuestion = await getWixelPendingQuestion({
1111
- ...base,
1112
- sessionId: args.handle
1113
- });
993
+ pendingQuestion = sessionId
994
+ ? await getWixelPendingQuestion({
995
+ ...base,
996
+ sessionId,
997
+ turnId: summary.turnId
998
+ })
999
+ : null;
1114
1000
  status = pendingQuestion ? 'question' : 'done';
1115
1001
  }
1116
1002
  }
1117
- else {
1118
- // No terminal event yet: parked on a question (custom_widget asks
1119
- // park the turn without a reply event) or still generating.
1003
+ else if (sessionId) {
1004
+ // Still working: parked on a question (custom_widget asks park the
1005
+ // turn without ending it) or generating.
1120
1006
  const activity = await getWixelSessionActivity({
1121
1007
  ...base,
1122
- sessionId: args.handle
1008
+ sessionId
1123
1009
  });
1124
1010
  pendingQuestion = activity.parkedQuestion;
1125
1011
  if (pendingQuestion)
@@ -1128,18 +1014,20 @@ export function addGetWixelTurnResultTool(server, authStrategy, options = {}) {
1128
1014
  console.log('[WixelResponseDump]', JSON.stringify({
1129
1015
  tool: 'get_wixel_turn_result',
1130
1016
  args,
1017
+ state: summary.state,
1131
1018
  status,
1132
1019
  assets,
1133
1020
  editorUrl,
1134
1021
  pendingQuestionId: pendingQuestion?.toolCallId ?? null
1135
1022
  }).slice(0, 30000));
1136
1023
  if (args.live && (status === 'done' || status === 'failed')) {
1137
- liveCards.forHandle(args.handle).finishedTurns.add(args.turnId);
1024
+ liveCards.forContext(task.contextId).finishedTasks.add(task.id);
1138
1025
  }
1139
1026
  const structuredContent = {
1140
1027
  success: true,
1141
1028
  status,
1142
- turnId: args.turnId,
1029
+ taskId: task.id,
1030
+ contextId: task.contextId,
1143
1031
  assets,
1144
1032
  editorUrl,
1145
1033
  pendingQuestion,
@@ -1158,6 +1046,9 @@ export function addGetWixelTurnResultTool(server, authStrategy, options = {}) {
1158
1046
  };
1159
1047
  }
1160
1048
  catch (error) {
1049
+ if (error instanceof WixelA2AError) {
1050
+ return a2aErrorResult('Could not read the Wixel task', error);
1051
+ }
1161
1052
  rethrowIfUnauthorized(error);
1162
1053
  console.error('❌ Error getting Wixel turn result:', error);
1163
1054
  const errorMessage = error.message;
@@ -1178,7 +1069,7 @@ async function resolveWixelAssetResults(args) {
1178
1069
  const { authHeaders } = args;
1179
1070
  const options = { conductorOverride: args.conductorOverride };
1180
1071
  // The model-facing schema caps these, but the live path feeds the resolver
1181
- // straight from a reply's text — cap here too, or one oversized reply
1072
+ // straight from a task's artifacts — cap here too, or one oversized reply
1182
1073
  // fans out into an unbounded number of authenticated lookups.
1183
1074
  const cappedAssetIds = (args.assetIds ?? []).slice(0, MAX_RESULT_ITEMS);
1184
1075
  const cappedMediaUrls = (args.mediaUrls ?? []).slice(0, MAX_RESULT_ITEMS);
@@ -1187,11 +1078,13 @@ async function resolveWixelAssetResults(args) {
1187
1078
  // are perfectly renderable.
1188
1079
  let projectId = null;
1189
1080
  try {
1190
- projectId = await getWixelSessionProjectId({
1191
- authHeaders,
1192
- conductorOverride: options.conductorOverride,
1193
- sessionId: args.handle
1194
- });
1081
+ projectId = args.sessionId
1082
+ ? await getWixelSessionProjectId({
1083
+ authHeaders,
1084
+ conductorOverride: options.conductorOverride,
1085
+ sessionId: args.sessionId
1086
+ })
1087
+ : null;
1195
1088
  }
1196
1089
  catch (projectError) {
1197
1090
  console.warn('[Wixel] editor link resolution failed:', projectError.message);
@@ -1365,34 +1258,32 @@ export function addGetWixelAssetResultTool(server, authStrategy, options = {}) {
1365
1258
  registerAppTool(server, 'get_wixel_asset_result', {
1366
1259
  title: 'Get Wixel asset result',
1367
1260
  description: `Render the result of a Wixel generation.
1368
- In widget-capable clients call it RIGHT AFTER send_wixel_message with the returned turnId: it renders a live result card that shows generation progress, asks the user any questions Wixel has, and displays the final asset — no polling needed.
1369
- Otherwise (or when a list_wixel_events reply already delivered assets) pass the assetIds and mediaUrls EXACTLY as returned by list_wixel_events (never invent ids — a turnId is NOT an asset id) and present the returned asset links as text, always including the Edit in Wixel link.`,
1261
+ In widget-capable clients call it RIGHT AFTER send_wixel_message with the returned contextId and taskId: it renders a live result card that shows generation progress, asks the user any questions Wixel has, and displays the final asset — no polling needed.
1262
+ Otherwise (or when get_wixel_task already reported the delivered assets) pass the assetIds and mediaUrls EXACTLY as get_wixel_task returned them (never invent ids — a taskId is NOT an asset id) and present the returned asset links as text, always including the Edit in Wixel link.`,
1370
1263
  inputSchema: {
1371
- handle: z.string().describe('Session handle from start_wixel_session'),
1372
- metaSiteId: z
1373
- .string()
1374
- .describe('Meta site ID from start_wixel_session'),
1264
+ contextId: contextIdInput,
1265
+ taskId: taskIdInput,
1266
+ metaSiteId: metaSiteIdInput,
1375
1267
  // Bounded: one reply delivers a handful of assets, and each entry costs
1376
1268
  // an authenticated lookup or a redirect chase.
1377
1269
  assetIds: z
1378
1270
  .array(z.string().min(1).max(200))
1379
1271
  .max(MAX_RESULT_ITEMS)
1380
1272
  .optional()
1381
- .describe('Asset ids delivered by the reply — copy from list_wixel_events assetIds'),
1273
+ .describe('Asset ids the turn delivered — copy from get_wixel_task assetIds'),
1382
1274
  mediaUrls: z
1383
1275
  .array(z.string().max(2048))
1384
1276
  .max(MAX_RESULT_ITEMS)
1385
1277
  .optional()
1386
- .describe('Media URLs embedded in the reply — copy from list_wixel_events mediaUrls'),
1278
+ .describe('Media URLs the turn delivered — copy from get_wixel_task mediaUrls'),
1279
+ // Required: besides heading the card, it is how the shared widget
1280
+ // tells this tool's invocation from get_wixel_pending_question's —
1281
+ // both carry the same conversation ids.
1387
1282
  prompt: z
1388
1283
  .string()
1389
- .max(4000)
1390
- .optional()
1391
- .describe("The user's original generation request, shown above the preview"),
1392
- turnId: z
1393
- .string()
1394
- .optional()
1395
- .describe('The turn returned by send_wixel_message. Widget clients: pass this (without assetIds/mediaUrls) to render the live card while Wixel generates.')
1284
+ .min(1)
1285
+ .max(WIXEL_MESSAGE_MAX_LENGTH)
1286
+ .describe("The user's original generation request, shown above the preview")
1396
1287
  },
1397
1288
  annotations: {
1398
1289
  readOnlyHint: true,
@@ -1401,26 +1292,29 @@ Otherwise (or when a list_wixel_events reply already delivered assets) pass the
1401
1292
  },
1402
1293
  _meta: QUESTION_TOOL_META
1403
1294
  }, async (args, extra) => {
1404
- setBi(extra, {
1405
- siteId: args.metaSiteId,
1406
- conversationId: args.handle
1407
- });
1295
+ setBi(extra, { siteId: args.metaSiteId });
1408
1296
  const authHeaders = await getSiteAuthHeaders(args.metaSiteId);
1297
+ const base = {
1298
+ authHeaders,
1299
+ conductorOverride: options.conductorOverride
1300
+ };
1409
1301
  try {
1302
+ // The card sends its answers into contextId, so it must be the
1303
+ // task's own; the task also names the session behind the editor link.
1304
+ const task = await getSiteTask({
1305
+ ...base,
1306
+ taskId: args.taskId,
1307
+ metaSiteId: args.metaSiteId
1308
+ });
1309
+ if (task.contextId !== args.contextId)
1310
+ return contextMismatchResult();
1311
+ const { sessionId } = summarizeTask(task);
1312
+ setBi(extra, {
1313
+ siteId: args.metaSiteId,
1314
+ ...(sessionId ? { conversationId: sessionId } : {})
1315
+ });
1410
1316
  if (!args.assetIds?.length && !args.mediaUrls?.length) {
1411
- // Live mode: nothing to resolve yet — the widget follows the turn.
1412
- if (!args.turnId) {
1413
- return {
1414
- isError: true,
1415
- content: [
1416
- {
1417
- type: 'text',
1418
- text: 'Pass the turnId from send_wixel_message (widget clients) or the assetIds/mediaUrls a reply delivered.'
1419
- }
1420
- ],
1421
- structuredContent: { success: false, error: 'missing turnId' }
1422
- };
1423
- }
1317
+ // Live mode: nothing to resolve yet — the widget follows the task.
1424
1318
  if (options.supportsWidgets?.() !== true) {
1425
1319
  return {
1426
1320
  isError: false,
@@ -1428,19 +1322,19 @@ Otherwise (or when a list_wixel_events reply already delivered assets) pass the
1428
1322
  {
1429
1323
  type: 'text',
1430
1324
  text: `This client cannot render the live card.
1431
- <agent_instruction>Poll list_wixel_events (handle: "${args.handle}", metaSiteId: "${args.metaSiteId}", waitSeconds: 25) until turnId "${args.turnId}" has a "reply" or "failed" event, then follow that response's instruction.</agent_instruction>`
1325
+ <agent_instruction>Poll get_wixel_task (taskId: "${args.taskId}", metaSiteId: "${args.metaSiteId}", waitSeconds: 25) until it reports the turn done, then follow that response's instruction.</agent_instruction>`
1432
1326
  }
1433
1327
  ],
1434
1328
  structuredContent: {
1435
1329
  success: false,
1436
1330
  pending: true,
1437
- turnId: args.turnId
1331
+ taskId: args.taskId
1438
1332
  }
1439
1333
  };
1440
1334
  }
1441
- const record = liveCards.forHandle(args.handle);
1442
- if (record.turns.has(args.turnId)) {
1443
- // Already rendered for this turn — a second card would show the
1335
+ const record = liveCards.forContext(task.contextId);
1336
+ if (record.tasks.has(task.id)) {
1337
+ // Already rendered for this task — a second card would show the
1444
1338
  // same generation twice.
1445
1339
  console.log('[WixelResponseDump]', JSON.stringify({
1446
1340
  tool: 'get_wixel_asset_result',
@@ -1452,7 +1346,7 @@ Otherwise (or when a list_wixel_events reply already delivered assets) pass the
1452
1346
  success: true,
1453
1347
  pending: true,
1454
1348
  duplicate: true,
1455
- turnId: args.turnId,
1349
+ taskId: args.taskId,
1456
1350
  assets: [],
1457
1351
  editorUrl: null,
1458
1352
  prompt: args.prompt ?? null
@@ -1463,7 +1357,7 @@ Otherwise (or when a list_wixel_events reply already delivered assets) pass the
1463
1357
  {
1464
1358
  type: 'text',
1465
1359
  text: `The live result card for this turn is already displayed.
1466
- <agent_instruction>Do not call any Wixel tool again for this generation. End your turn now WITHOUT writing any message — no summary, no "Wixel is working on it", nothing: the card already says it, and a sentence under it is redundant. If your client forces a reply, use at most two words.</agent_instruction>`
1360
+ <agent_instruction>Do not call any Wixel tool again for this generation. ${END_TURN_SILENTLY}</agent_instruction>`
1467
1361
  },
1468
1362
  {
1469
1363
  type: 'text',
@@ -1473,12 +1367,12 @@ Otherwise (or when a list_wixel_events reply already delivered assets) pass the
1473
1367
  structuredContent: duplicateContent
1474
1368
  };
1475
1369
  }
1476
- record.turns.set(args.turnId, Date.now());
1370
+ record.tasks.set(task.id, Date.now());
1477
1371
  console.log('[WixelResponseDump]', JSON.stringify({ tool: 'get_wixel_asset_result', args, live: true }));
1478
1372
  const liveContent = {
1479
1373
  success: true,
1480
1374
  pending: true,
1481
- turnId: args.turnId,
1375
+ taskId: args.taskId,
1482
1376
  assets: [],
1483
1377
  editorUrl: null,
1484
1378
  prompt: args.prompt ?? null
@@ -1489,7 +1383,7 @@ Otherwise (or when a list_wixel_events reply already delivered assets) pass the
1489
1383
  {
1490
1384
  type: 'text',
1491
1385
  text: `The live result card is displayed to the user. It shows generation progress, lets the user answer any question Wixel asks, and displays the final asset with edit/download actions — all inside the card.
1492
- <agent_instruction>Do NOT poll list_wixel_events and do NOT call get_wixel_pending_question — the card handles the rest of this generation itself. End your turn now WITHOUT writing any message — no summary, no "Wixel is working on it", nothing: the card already says it, and a sentence under it is redundant. If your client forces a reply, use at most two words.</agent_instruction>`
1386
+ <agent_instruction>Do NOT poll get_wixel_task and do NOT call get_wixel_pending_question — the card handles the rest of this generation itself. ${END_TURN_SILENTLY}</agent_instruction>`
1493
1387
  },
1494
1388
  { type: 'text', text: JSON.stringify(liveContent) }
1495
1389
  ],
@@ -1497,10 +1391,10 @@ Otherwise (or when a list_wixel_events reply already delivered assets) pass the
1497
1391
  _meta: widgetResultMeta(server)
1498
1392
  };
1499
1393
  }
1500
- // A live card already displays these assets (list_wixel_events or
1501
- // get_wixel_turn_result recorded them for a live turn) — rendering
1394
+ // A live card already displays these assets (get_wixel_task or
1395
+ // get_wixel_turn_result recorded them for a live task) — rendering
1502
1396
  // them again would put the result in the chat twice.
1503
- const shown = liveCards.get(args.handle)?.shownAssets;
1397
+ const shown = liveCards.get(task.contextId)?.shownAssets;
1504
1398
  const requested = [...(args.assetIds ?? []), ...(args.mediaUrls ?? [])];
1505
1399
  if (options.supportsWidgets?.() === true &&
1506
1400
  shown?.size &&
@@ -1523,7 +1417,7 @@ Otherwise (or when a list_wixel_events reply already delivered assets) pass the
1523
1417
  {
1524
1418
  type: 'text',
1525
1419
  text: `This result is already displayed by the live result card.
1526
- <agent_instruction>Do NOT present the asset again (no links, no image). End your turn now WITHOUT writing any message — no summary, no "Wixel is working on it", nothing: the card already says it, and a sentence under it is redundant. If your client forces a reply, use at most two words.</agent_instruction>`
1420
+ <agent_instruction>Do NOT present the asset again (no links, no image). ${END_TURN_SILENTLY}</agent_instruction>`
1527
1421
  },
1528
1422
  { type: 'text', text: JSON.stringify(duplicateContent) }
1529
1423
  ],
@@ -1531,9 +1425,8 @@ Otherwise (or when a list_wixel_events reply already delivered assets) pass the
1531
1425
  };
1532
1426
  }
1533
1427
  const { assets, editorUrl } = await resolveWixelAssetResults({
1534
- authHeaders,
1535
- conductorOverride: options.conductorOverride,
1536
- handle: args.handle,
1428
+ ...base,
1429
+ sessionId,
1537
1430
  metaSiteId: args.metaSiteId,
1538
1431
  assetIds: args.assetIds,
1539
1432
  mediaUrls: args.mediaUrls
@@ -1589,6 +1482,9 @@ Otherwise (or when a list_wixel_events reply already delivered assets) pass the
1589
1482
  };
1590
1483
  }
1591
1484
  catch (error) {
1485
+ if (error instanceof WixelA2AError) {
1486
+ return a2aErrorResult('Could not read the Wixel task', error);
1487
+ }
1592
1488
  rethrowIfUnauthorized(error);
1593
1489
  console.error('❌ Error resolving Wixel asset result:', error);
1594
1490
  const errorMessage = error.message;
@@ -1605,9 +1501,8 @@ export function addWixelSessionTools(server, authStrategy, options = {}) {
1605
1501
  ...options,
1606
1502
  liveCards: options.liveCards ?? createLiveCardRegistry()
1607
1503
  };
1608
- addStartWixelSessionTool(server, authStrategy, shared);
1609
1504
  addSendWixelMessageTool(server, authStrategy, shared);
1610
- addListWixelEventsTool(server, authStrategy, shared);
1505
+ addGetWixelTaskTool(server, authStrategy, shared);
1611
1506
  addGetWixelPendingQuestionTool(server, authStrategy, shared);
1612
1507
  addGetWixelAssetResultTool(server, authStrategy, shared);
1613
1508
  addGetWixelStylePresetsTool(server, authStrategy, shared);