wu-framework 1.1.15 → 1.1.17

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 (88) hide show
  1. package/README.md +52 -20
  2. package/dist/wu-framework.cjs.js +1 -1
  3. package/dist/wu-framework.cjs.js.map +1 -1
  4. package/dist/wu-framework.dev.js +15511 -15146
  5. package/dist/wu-framework.dev.js.map +1 -1
  6. package/dist/wu-framework.esm.js +1 -1
  7. package/dist/wu-framework.esm.js.map +1 -1
  8. package/dist/wu-framework.umd.js +1 -1
  9. package/dist/wu-framework.umd.js.map +1 -1
  10. package/package.json +166 -161
  11. package/src/adapters/angular/ai.js +30 -30
  12. package/src/adapters/angular/index.d.ts +154 -154
  13. package/src/adapters/angular/index.js +932 -932
  14. package/src/adapters/angular.d.ts +3 -3
  15. package/src/adapters/angular.js +3 -3
  16. package/src/adapters/index.js +168 -168
  17. package/src/adapters/lit/ai.js +20 -20
  18. package/src/adapters/lit/index.d.ts +120 -120
  19. package/src/adapters/lit/index.js +721 -721
  20. package/src/adapters/lit.d.ts +3 -3
  21. package/src/adapters/lit.js +3 -3
  22. package/src/adapters/preact/ai.js +33 -33
  23. package/src/adapters/preact/index.d.ts +108 -108
  24. package/src/adapters/preact/index.js +661 -661
  25. package/src/adapters/preact.d.ts +3 -3
  26. package/src/adapters/preact.js +3 -3
  27. package/src/adapters/react/index.js +48 -54
  28. package/src/adapters/react.d.ts +3 -3
  29. package/src/adapters/react.js +3 -3
  30. package/src/adapters/shared.js +64 -64
  31. package/src/adapters/solid/ai.js +32 -32
  32. package/src/adapters/solid/index.d.ts +101 -101
  33. package/src/adapters/solid/index.js +586 -586
  34. package/src/adapters/solid.d.ts +3 -3
  35. package/src/adapters/solid.js +3 -3
  36. package/src/adapters/svelte/ai.js +31 -31
  37. package/src/adapters/svelte/index.d.ts +166 -166
  38. package/src/adapters/svelte/index.js +798 -798
  39. package/src/adapters/svelte.d.ts +3 -3
  40. package/src/adapters/svelte.js +3 -3
  41. package/src/adapters/vanilla/ai.js +30 -30
  42. package/src/adapters/vanilla/index.d.ts +179 -179
  43. package/src/adapters/vanilla/index.js +785 -785
  44. package/src/adapters/vanilla.d.ts +3 -3
  45. package/src/adapters/vanilla.js +3 -3
  46. package/src/adapters/vue/ai.js +52 -52
  47. package/src/adapters/vue/index.d.ts +299 -299
  48. package/src/adapters/vue/index.js +610 -610
  49. package/src/adapters/vue.d.ts +3 -3
  50. package/src/adapters/vue.js +3 -3
  51. package/src/ai/wu-ai-actions.js +261 -261
  52. package/src/ai/wu-ai-agent.js +546 -546
  53. package/src/ai/wu-ai-browser-primitives.js +354 -354
  54. package/src/ai/wu-ai-browser.js +380 -380
  55. package/src/ai/wu-ai-context.js +332 -332
  56. package/src/ai/wu-ai-conversation.js +613 -613
  57. package/src/ai/wu-ai-orchestrate.js +1021 -1021
  58. package/src/ai/wu-ai-permissions.js +381 -381
  59. package/src/ai/wu-ai-provider.js +700 -700
  60. package/src/ai/wu-ai-schema.js +225 -225
  61. package/src/ai/wu-ai-triggers.js +396 -396
  62. package/src/ai/wu-ai.js +804 -804
  63. package/src/core/wu-app.js +236 -236
  64. package/src/core/wu-cache.js +498 -477
  65. package/src/core/wu-core.js +1412 -1398
  66. package/src/core/wu-error-boundary.js +396 -382
  67. package/src/core/wu-event-bus.js +390 -348
  68. package/src/core/wu-hooks.js +350 -350
  69. package/src/core/wu-html-parser.js +199 -190
  70. package/src/core/wu-iframe-sandbox.js +328 -328
  71. package/src/core/wu-loader.js +385 -273
  72. package/src/core/wu-logger.js +142 -134
  73. package/src/core/wu-manifest.js +532 -509
  74. package/src/core/wu-mcp-bridge.js +432 -432
  75. package/src/core/wu-overrides.js +510 -510
  76. package/src/core/wu-performance.js +228 -228
  77. package/src/core/wu-plugin.js +401 -348
  78. package/src/core/wu-prefetch.js +414 -414
  79. package/src/core/wu-proxy-sandbox.js +477 -476
  80. package/src/core/wu-sandbox.js +779 -779
  81. package/src/core/wu-script-executor.js +161 -113
  82. package/src/core/wu-snapshot-sandbox.js +227 -227
  83. package/src/core/wu-store.js +13 -3
  84. package/src/core/wu-strategies.js +256 -256
  85. package/src/core/wu-style-bridge.js +477 -477
  86. package/src/index.d.ts +317 -0
  87. package/src/index.js +234 -224
  88. package/src/utils/dependency-resolver.js +327 -327
@@ -1,1021 +1,1021 @@
1
- /**
2
- * WU-AI-ORCHESTRATE: Cross-Micro-App AI Coordination (Paradigm 4)
3
- *
4
- * The fourth paradigm of wu.ai:
5
- * Paradigm 1 — App sends messages to LLM (conversation)
6
- * Paradigm 2 — External LLM calls into app (tools/WebMCP)
7
- * Paradigm 3 — AI as autonomous director (agent loop)
8
- * Paradigm 4 — AI as microfrontend glue (this file)
9
- *
10
- * The Problem:
11
- * In microfrontend architectures, apps are isolated by design.
12
- * Cross-app coordination requires manual wiring through events
13
- * and shared state. As apps grow, wiring becomes n² complexity.
14
- *
15
- * The Solution:
16
- * Each micro-app declares its capabilities to the AI layer.
17
- * The AI understands the semantic meaning of each capability
18
- * and can resolve natural-language intents by calling the right
19
- * actions across the right apps — without tight coupling.
20
- *
21
- * Key Concepts:
22
- * - Capability: An action scoped to a specific micro-app
23
- * Registered as 'appName:actionName', cleaned up on unmount.
24
- * - Intent: A natural-language cross-app request resolved in
25
- * a single conversation turn with an orchestrator system prompt.
26
- * - Capability Map: The AI's understanding of system topology —
27
- * which apps exist and what each can do.
28
- *
29
- * This module does NOT replace actions, triggers, or agents.
30
- * It enriches them with cross-app topology awareness.
31
- *
32
- * API (accessible via wu.ai):
33
- * wu.ai.capability(app, name, config) → Register app-scoped capability
34
- * wu.ai.intent(description, options) → Resolve cross-app intent
35
- * wu.ai.removeApp(appName) → Cleanup on unmount
36
- * wu.ai.workflow(name, config) → Register reusable AI workflow
37
- * wu.ai.runWorkflow(name, params, opts) → Execute a registered workflow
38
- */
39
-
40
- import { logger } from '../core/wu-logger.js';
41
- import { clickElement, typeIntoElement } from './wu-ai-browser-primitives.js';
42
-
43
- // ─── Constants ──────────────────────────────────────────────────
44
-
45
- const INTENT_NAMESPACE_PREFIX = 'intent:';
46
-
47
- // ─── Deterministic Step Actions ─────────────────────────────────
48
-
49
- /**
50
- * Execute a single deterministic workflow step.
51
- * No AI needed — directly calls browser primitives.
52
- *
53
- * @param {object} step - Step definition
54
- * @param {string} step.action - 'click' | 'type' | 'navigate' | 'wait' | 'emit' | 'setState'
55
- * @param {object} params - Interpolated params
56
- * @returns {{ success: boolean, detail?: string, error?: string }}
57
- */
58
- function executeDeterministicStep(step, eventBus, store) {
59
- switch (step.action) {
60
- case 'click': {
61
- const result = clickElement(step.selector, step.text);
62
- if (result.error) return { success: false, error: result.error };
63
- return { success: true, detail: `Clicked: ${step.selector || step.text}` };
64
- }
65
-
66
- case 'type': {
67
- const result = typeIntoElement(step.selector, step.value, {
68
- clear: step.clear ?? true,
69
- submit: step.submit ?? false,
70
- });
71
- if (result.error) return { success: false, error: result.error };
72
- return { success: true, detail: `Typed "${step.value}" into ${step.selector}` };
73
- }
74
-
75
- case 'navigate': {
76
- if (eventBus && step.section) {
77
- eventBus.emit('nav:section', { section: step.section }, { appName: 'wu-ai' });
78
- return { success: true, detail: `Navigated to section: ${step.section}` };
79
- }
80
- if (step.selector) {
81
- const result = clickElement(step.selector, step.text);
82
- if (result.error) return { success: false, error: result.error };
83
- return { success: true, detail: `Navigated via click: ${step.selector}` };
84
- }
85
- return { success: false, error: 'navigate requires "section" or "selector"' };
86
- }
87
-
88
- case 'wait': {
89
- // Wait is handled by the runner (delay + optional selector poll)
90
- return { success: true, detail: `Wait: ${step.ms || 0}ms` };
91
- }
92
-
93
- case 'emit': {
94
- if (!eventBus) return { success: false, error: 'eventBus not available' };
95
- eventBus.emit(step.event, step.data || {}, { appName: 'wu-ai' });
96
- return { success: true, detail: `Emitted: ${step.event}` };
97
- }
98
-
99
- case 'setState': {
100
- if (!store) return { success: false, error: 'store not available' };
101
- store.set(step.path, step.value);
102
- return { success: true, detail: `Set state: ${step.path}` };
103
- }
104
-
105
- default:
106
- return { success: false, error: `Unknown action: ${step.action}` };
107
- }
108
- }
109
-
110
- /**
111
- * Wait for a selector to appear in the DOM (with timeout).
112
- *
113
- * @param {string} selector
114
- * @param {number} timeout - ms
115
- * @returns {Promise<boolean>}
116
- */
117
- function waitForSelector(selector, timeout = 5000) {
118
- return new Promise((resolve) => {
119
- if (document.querySelector(selector)) {
120
- resolve(true);
121
- return;
122
- }
123
-
124
- const interval = 100;
125
- let elapsed = 0;
126
- const timer = setInterval(() => {
127
- elapsed += interval;
128
- if (document.querySelector(selector)) {
129
- clearInterval(timer);
130
- resolve(true);
131
- } else if (elapsed >= timeout) {
132
- clearInterval(timer);
133
- resolve(false);
134
- }
135
- }, interval);
136
- });
137
- }
138
-
139
- /**
140
- * Simple delay.
141
- */
142
- function delay(ms) {
143
- return new Promise((resolve) => setTimeout(resolve, ms));
144
- }
145
-
146
- // ─── WuAIOrchestrate ────────────────────────────────────────────
147
-
148
- export class WuAIOrchestrate {
149
- /**
150
- * @param {object} deps
151
- * @param {import('./wu-ai-actions.js').WuAIActions} deps.actions
152
- * @param {import('./wu-ai-conversation.js').WuAIConversation} deps.conversation
153
- * @param {import('./wu-ai-context.js').WuAIContext} deps.context
154
- * @param {import('./wu-ai-permissions.js').WuAIPermissions} deps.permissions
155
- * @param {object} deps.eventBus
156
- */
157
- constructor({ actions, conversation, context, permissions, eventBus, agent, store }) {
158
- this._actions = actions;
159
- this._conversation = conversation;
160
- this._context = context;
161
- this._permissions = permissions;
162
- this._eventBus = eventBus;
163
- this._agent = agent; // WuAIAgent — for workflow execution
164
- this._store = store; // WuStore — for deterministic setState steps
165
-
166
- // appName → Map<actionName, { description, qualifiedName }>
167
- this._capabilities = new Map();
168
-
169
- // name → { goal, steps, parameters, provider, ... }
170
- this._workflows = new Map();
171
-
172
- this._config = {
173
- defaultProvider: null,
174
- defaultTemperature: 0.3, // lower temp for orchestration
175
- };
176
-
177
- this._stats = {
178
- totalIntents: 0,
179
- resolvedIntents: 0,
180
- failedIntents: 0,
181
- workflowsRegistered: 0,
182
- workflowsExecuted: 0,
183
- };
184
- }
185
-
186
- /**
187
- * Post-init configuration.
188
- *
189
- * @param {object} config
190
- * @param {string} [config.defaultProvider] - Default provider for intents
191
- * @param {number} [config.defaultTemperature] - Default temperature for intents
192
- */
193
- configure(config) {
194
- if (config.defaultProvider !== undefined) this._config.defaultProvider = config.defaultProvider;
195
- if (config.defaultTemperature !== undefined) this._config.defaultTemperature = config.defaultTemperature;
196
- }
197
-
198
- // ─── Capability Registration ────────────────────────────────────
199
-
200
- /**
201
- * Register a capability scoped to a micro-app.
202
- *
203
- * Under the hood this registers a normal action with the qualified
204
- * name 'appName:actionName' so the LLM can call it directly.
205
- * The capability map is tracked separately for lifecycle management
206
- * (removeApp) and system prompt enrichment.
207
- *
208
- * @param {string} appName - The micro-app name (e.g., 'orders', 'dashboard')
209
- * @param {string} actionName - The capability name (e.g., 'getRecent', 'updateKPIs')
210
- * @param {object} config - Same as wu.ai.action() config:
211
- * { description, parameters, handler, confirm?, permissions?, dangerous? }
212
- */
213
- register(appName, actionName, config) {
214
- if (!appName || !actionName) {
215
- throw new Error('[wu-ai] capability() requires both appName and actionName');
216
- }
217
- if (!config || typeof config.handler !== 'function') {
218
- throw new Error(`[wu-ai] capability '${appName}:${actionName}' must have a handler function`);
219
- }
220
-
221
- const qualifiedName = `${appName}:${actionName}`;
222
-
223
- // Track in capability map
224
- if (!this._capabilities.has(appName)) {
225
- this._capabilities.set(appName, new Map());
226
- }
227
- this._capabilities.get(appName).set(actionName, {
228
- description: config.description || actionName,
229
- qualifiedName,
230
- });
231
-
232
- // Register as a normal action with enriched description
233
- this._actions.register(qualifiedName, {
234
- ...config,
235
- description: `[${appName}] ${config.description || actionName}`,
236
- });
237
-
238
- logger.wuDebug(`[wu-ai] Capability registered: ${qualifiedName}`);
239
- }
240
-
241
- /**
242
- * Remove a single capability.
243
- *
244
- * @param {string} appName
245
- * @param {string} actionName
246
- */
247
- unregister(appName, actionName) {
248
- const appCaps = this._capabilities.get(appName);
249
- if (!appCaps) return;
250
-
251
- const qualifiedName = `${appName}:${actionName}`;
252
- appCaps.delete(actionName);
253
- this._actions.unregister(qualifiedName);
254
-
255
- // Clean up empty app entry
256
- if (appCaps.size === 0) {
257
- this._capabilities.delete(appName);
258
- }
259
- }
260
-
261
- /**
262
- * Remove all capabilities for a micro-app (unmount cleanup).
263
- *
264
- * Call this when a micro-app is unmounted to prevent stale
265
- * capabilities from appearing in the AI's capability map.
266
- *
267
- * @param {string} appName
268
- * @returns {number} Number of capabilities removed
269
- */
270
- removeApp(appName) {
271
- const appCaps = this._capabilities.get(appName);
272
- if (!appCaps) return 0;
273
-
274
- let removed = 0;
275
- for (const [actionName] of appCaps) {
276
- const qualifiedName = `${appName}:${actionName}`;
277
- this._actions.unregister(qualifiedName);
278
- removed++;
279
- }
280
-
281
- this._capabilities.delete(appName);
282
-
283
- logger.wuDebug(`[wu-ai] All capabilities removed for app '${appName}' (${removed})`);
284
-
285
- this._eventBus.emit('ai:app:removed', {
286
- appName,
287
- capabilitiesRemoved: removed,
288
- }, { appName: 'wu-ai' });
289
-
290
- return removed;
291
- }
292
-
293
- // ─── Capability Map ─────────────────────────────────────────────
294
-
295
- /**
296
- * Get the full capability map grouped by app.
297
- *
298
- * Used for system prompt enrichment and debugging.
299
- *
300
- * @returns {object} { appName: [{ action, description }], ... }
301
- */
302
- getCapabilityMap() {
303
- const map = {};
304
- for (const [appName, actions] of this._capabilities) {
305
- map[appName] = [];
306
- for (const [, meta] of actions) {
307
- map[appName].push({
308
- action: meta.qualifiedName,
309
- description: meta.description,
310
- });
311
- }
312
- }
313
- return map;
314
- }
315
-
316
- /**
317
- * Get app names that have registered capabilities.
318
- *
319
- * @returns {string[]}
320
- */
321
- getRegisteredApps() {
322
- return [...this._capabilities.keys()];
323
- }
324
-
325
- /**
326
- * Check if an app has registered capabilities.
327
- *
328
- * @param {string} appName
329
- * @returns {boolean}
330
- */
331
- hasApp(appName) {
332
- return this._capabilities.has(appName);
333
- }
334
-
335
- /**
336
- * Get the total number of registered capabilities across all apps.
337
- *
338
- * @returns {number}
339
- */
340
- getTotalCapabilities() {
341
- let count = 0;
342
- for (const actions of this._capabilities.values()) {
343
- count += actions.size;
344
- }
345
- return count;
346
- }
347
-
348
- // ─── Intent Resolution ──────────────────────────────────────────
349
-
350
- /**
351
- * Resolve a cross-app intent in a single conversation turn.
352
- *
353
- * The AI receives:
354
- * - The full capability map (what each app can do)
355
- * - Current application state (via context)
356
- * - Mounted apps list
357
- * - All registered tools (capabilities are tools)
358
- *
359
- * Unlike agent(), this is NOT a multi-step autonomous loop.
360
- * The LLM resolves the intent in one logical request (which may
361
- * include multiple tool calls within the conversation's tool-call
362
- * loop, but conceptually is a single turn).
363
- *
364
- * Unlike send(), the namespace is ephemeral and auto-cleaned,
365
- * and the system prompt is auto-built with the capability map.
366
- *
367
- * @param {string} description - Natural language intent
368
- * e.g., "Show me the top customer by order count"
369
- * e.g., "Update dashboard stats and notify the topbar"
370
- * @param {object} [options]
371
- * @param {string[]} [options.plan] - Optional action sequence hint.
372
- * The AI uses this as guidance but can deviate if needed.
373
- * e.g., ['orders:getRecent', 'customers:lookup']
374
- * @param {string} [options.provider] - LLM provider override
375
- * @param {number} [options.temperature] - Temperature override
376
- * @param {number} [options.maxTokens] - Max tokens override
377
- * @param {AbortSignal} [options.signal] - Abort signal
378
- * @param {string|object} [options.responseFormat] - Response format
379
- * @returns {Promise<{
380
- * content: string,
381
- * tool_results: Array,
382
- * usage: object|null,
383
- * resolved: boolean,
384
- * appsInvolved: string[]
385
- * }>}
386
- */
387
- async resolve(description, options = {}) {
388
- if (!description || typeof description !== 'string') {
389
- throw new Error('[wu-ai] intent() requires a description string');
390
- }
391
-
392
- this._stats.totalIntents++;
393
- const namespace = this._generateNamespace();
394
-
395
- // Collect fresh context before building the prompt
396
- if (this._context) {
397
- try {
398
- await this._context.collect();
399
- } catch {
400
- // Context collection is best-effort
401
- }
402
- }
403
-
404
- const systemPrompt = this._buildOrchestratorPrompt(options);
405
-
406
- this._eventBus.emit('ai:intent:start', {
407
- description: description.slice(0, 200),
408
- namespace,
409
- capabilities: this.getTotalCapabilities(),
410
- }, { appName: 'wu-ai' });
411
-
412
- try {
413
- const response = await this._conversation.send(description, {
414
- namespace,
415
- systemPrompt,
416
- provider: options.provider || this._config.defaultProvider,
417
- temperature: options.temperature ?? this._config.defaultTemperature,
418
- maxTokens: options.maxTokens,
419
- signal: options.signal,
420
- responseFormat: options.responseFormat,
421
- });
422
-
423
- const toolResults = response.tool_results || [];
424
- const appsInvolved = this._extractInvolvedApps(toolResults);
425
- const resolved = !!(response.content);
426
-
427
- if (resolved) {
428
- this._stats.resolvedIntents++;
429
- } else {
430
- this._stats.failedIntents++;
431
- }
432
-
433
- const result = {
434
- content: response.content || '',
435
- tool_results: toolResults,
436
- usage: response.usage || null,
437
- resolved,
438
- appsInvolved,
439
- };
440
-
441
- this._eventBus.emit('ai:intent:resolved', {
442
- description: description.slice(0, 200),
443
- resolved,
444
- appsInvolved,
445
- }, { appName: 'wu-ai' });
446
-
447
- return result;
448
- } catch (err) {
449
- this._stats.failedIntents++;
450
-
451
- this._eventBus.emit('ai:intent:error', {
452
- description: description.slice(0, 200),
453
- error: err.message,
454
- }, { appName: 'wu-ai' });
455
-
456
- throw err;
457
- } finally {
458
- // Always clean up the ephemeral namespace
459
- this._conversation.deleteNamespace(namespace);
460
- }
461
- }
462
-
463
- // ─── System Prompt Builder ──────────────────────────────────────
464
-
465
- /**
466
- * Build the orchestrator system prompt with full capability map.
467
- *
468
- * This method is also available to other modules (triggers, agents)
469
- * that want capability-aware system prompts.
470
- *
471
- * @param {object} [options]
472
- * @param {string[]} [options.plan] - Optional action sequence hint
473
- * @returns {string}
474
- */
475
- buildOrchestratorPrompt(options = {}) {
476
- return this._buildOrchestratorPrompt(options);
477
- }
478
-
479
- /** @private */
480
- _buildOrchestratorPrompt(options = {}) {
481
- const parts = [];
482
-
483
- parts.push(
484
- 'You are an AI orchestrator for a microfrontend application.',
485
- 'Multiple independent apps are mounted, each with specific capabilities.',
486
- 'Resolve cross-app requests by calling the right capabilities in the right order.',
487
- '',
488
- 'RULES:',
489
- '- Call capabilities (tools) to gather data or trigger actions.',
490
- '- You may call multiple capabilities from different apps if needed.',
491
- '- Synthesize results into a clear, actionable response.',
492
- '- If a required app is not available or lacks a capability, explain what is missing.',
493
- '',
494
- );
495
-
496
- // Capability map
497
- const capMap = this.getCapabilityMap();
498
- const appNames = Object.keys(capMap);
499
-
500
- if (appNames.length > 0) {
501
- parts.push('CAPABILITY MAP:');
502
- for (const appName of appNames) {
503
- parts.push(` ${appName}:`);
504
- for (const cap of capMap[appName]) {
505
- parts.push(` - ${cap.action}: ${cap.description}`);
506
- }
507
- }
508
- parts.push('');
509
- } else {
510
- parts.push(
511
- 'NOTE: No app capabilities are registered. Answer based on available context only.',
512
- '',
513
- );
514
- }
515
-
516
- // Optional plan hint
517
- if (options.plan && options.plan.length > 0) {
518
- parts.push(
519
- 'SUGGESTED PLAN (follow this unless a better approach is evident):',
520
- );
521
- for (let i = 0; i < options.plan.length; i++) {
522
- parts.push(` ${i + 1}. ${options.plan[i]}`);
523
- }
524
- parts.push('');
525
- }
526
-
527
- // Context snapshot (state, mounted apps)
528
- const snapshot = this._context?.getSnapshot();
529
- if (snapshot?._mountedApps?.length) {
530
- parts.push(`MOUNTED APPS: ${snapshot._mountedApps.join(', ')}`, '');
531
- }
532
- if (snapshot?._store && Object.keys(snapshot._store).length > 0) {
533
- parts.push(
534
- 'CURRENT STATE:',
535
- JSON.stringify(snapshot._store, null, 2),
536
- '',
537
- );
538
- }
539
-
540
- return parts.join('\n');
541
- }
542
-
543
- // ─── Workflows ─────────────────────────────────────────────────
544
-
545
- /**
546
- * Register a reusable AI workflow.
547
- *
548
- * A workflow is a named, parameterized recipe that the AI agent
549
- * follows step by step. Think of it as a macro: you define it once,
550
- * then run it whenever you need with different parameters.
551
- *
552
- * The AI receives the steps as instructions and uses browser actions
553
- * (screenshot, click, type) plus any registered capabilities/actions
554
- * to execute them. You can watch every step in real time.
555
- *
556
- * @param {string} name - Workflow name (e.g., 'register-user')
557
- * @param {object} config
558
- * @param {string} config.description - What this workflow does
559
- * @param {string[]} config.steps - Step-by-step instructions for the AI
560
- * @param {object} [config.parameters] - Parameter definitions for interpolation
561
- * e.g., { name: { type: 'string', required: true }, email: { type: 'string' } }
562
- * @param {number} [config.maxSteps=15] - Max agent steps allowed
563
- * @param {string} [config.provider] - LLM provider to use
564
- * @param {number} [config.temperature] - Temperature (default: 0.2 for precision)
565
- *
566
- * @example
567
- * // ── AI Mode (default): steps are natural language ──
568
- * wu.ai.workflow('register-user', {
569
- * description: 'Register a new user in the system',
570
- * steps: [
571
- * 'Navigate to the Customers section',
572
- * 'Click the "Add Customer" button',
573
- * 'Fill in the name field with {{name}}',
574
- * 'Click Submit',
575
- * ],
576
- * parameters: { name: { type: 'string', required: true } },
577
- * });
578
- *
579
- * // ── Deterministic Mode: steps are exact actions, NO AI NEEDED ──
580
- * wu.ai.workflow('register-user', {
581
- * mode: 'deterministic',
582
- * description: 'Register a new user',
583
- * steps: [
584
- * { action: 'navigate', section: 'customers' },
585
- * { action: 'click', selector: '#add-customer-btn' },
586
- * { action: 'type', selector: '#name', value: '{{name}}' },
587
- * { action: 'type', selector: '#email', value: '{{email}}' },
588
- * { action: 'click', selector: '#submit-btn' },
589
- * { action: 'wait', selector: '.success-message', timeout: 5000 },
590
- * ],
591
- * parameters: {
592
- * name: { type: 'string', required: true },
593
- * email: { type: 'string', required: true },
594
- * },
595
- * });
596
- */
597
- registerWorkflow(name, config) {
598
- if (!name) {
599
- throw new Error('[wu-ai] workflow() requires a name');
600
- }
601
- if (!config || !config.steps || !Array.isArray(config.steps) || config.steps.length === 0) {
602
- throw new Error(`[wu-ai] workflow '${name}' must have a non-empty steps array`);
603
- }
604
-
605
- // Detect mode: if steps are objects with 'action', it's deterministic
606
- const mode = config.mode || (
607
- config.steps.length > 0 && typeof config.steps[0] === 'object' && config.steps[0].action
608
- ? 'deterministic'
609
- : 'ai'
610
- );
611
-
612
- this._workflows.set(name, {
613
- description: config.description || name,
614
- steps: config.steps,
615
- mode,
616
- parameters: config.parameters || {},
617
- maxSteps: config.maxSteps ?? 15,
618
- provider: config.provider || null,
619
- temperature: config.temperature ?? 0.2,
620
- });
621
-
622
- this._stats.workflowsRegistered++;
623
-
624
- logger.wuDebug(`[wu-ai] Workflow registered: '${name}' (${config.steps.length} steps)`);
625
- }
626
-
627
- /**
628
- * Execute a registered workflow with parameters.
629
- *
630
- * Returns an async generator (like agent) — you iterate over it
631
- * to observe each step in real time.
632
- *
633
- * @param {string} name - Workflow name
634
- * @param {object} [params={}] - Parameters to interpolate into steps
635
- * e.g., { name: 'Juan Pérez', email: 'juan@test.com' }
636
- * @param {object} [options={}]
637
- * @param {Function} [options.onStep] - Callback per step
638
- * @param {Function} [options.shouldContinue] - Human-in-the-loop gate
639
- * @param {AbortSignal} [options.signal] - Abort signal
640
- * @returns {AsyncGenerator<AgentStepResult>}
641
- *
642
- * @example
643
- * for await (const step of wu.ai.runWorkflow('register-user', {
644
- * name: 'Juan Pérez',
645
- * email: 'juan@test.com',
646
- * })) {
647
- * console.log(`Paso ${step.step}: ${step.content}`);
648
- * if (step.type === 'done') console.log('Workflow completado!');
649
- * }
650
- *
651
- * // With human approval per step:
652
- * for await (const step of wu.ai.runWorkflow('register-user', params, {
653
- * shouldContinue: (step) => confirm(`¿Continuar? ${step.content?.slice(0, 60)}`),
654
- * })) {
655
- * renderStep(step);
656
- * }
657
- */
658
- async *executeWorkflow(name, params = {}, options = {}) {
659
- const workflow = this._workflows.get(name);
660
- if (!workflow) {
661
- throw new Error(`[wu-ai] Workflow '${name}' is not registered`);
662
- }
663
-
664
- // Validate required parameters
665
- for (const [paramName, paramConfig] of Object.entries(workflow.parameters)) {
666
- if (paramConfig.required && (params[paramName] === undefined || params[paramName] === null)) {
667
- throw new Error(`[wu-ai] Workflow '${name}' requires parameter '${paramName}'`);
668
- }
669
- }
670
-
671
- this._stats.workflowsExecuted++;
672
-
673
- // Branch on mode
674
- if (workflow.mode === 'deterministic') {
675
- yield* this._executeDeterministic(name, workflow, params, options);
676
- } else {
677
- yield* this._executeWithAgent(name, workflow, params, options);
678
- }
679
- }
680
-
681
- /**
682
- * Execute workflow using the AI agent (natural language steps).
683
- * @private
684
- */
685
- async *_executeWithAgent(name, workflow, params, options) {
686
- if (!this._agent) {
687
- throw new Error('[wu-ai] Agent module not available for workflow execution');
688
- }
689
-
690
- // Interpolate parameters into string steps
691
- const interpolatedSteps = workflow.steps.map(step => {
692
- let result = step;
693
- for (const [key, value] of Object.entries(params)) {
694
- result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), String(value));
695
- }
696
- return result;
697
- });
698
-
699
- const goal = this._buildWorkflowGoal(workflow, interpolatedSteps, params);
700
-
701
- this._eventBus.emit('ai:workflow:start', {
702
- workflow: name,
703
- mode: 'ai',
704
- params,
705
- steps: interpolatedSteps.length,
706
- }, { appName: 'wu-ai' });
707
-
708
- let finalStep = null;
709
-
710
- try {
711
- yield* this._agent.run(goal, {
712
- maxSteps: workflow.maxSteps,
713
- provider: options.provider || workflow.provider || this._config.defaultProvider,
714
- temperature: workflow.temperature ?? 0.2,
715
- onStep: (step) => {
716
- finalStep = step;
717
- if (options.onStep) options.onStep(step);
718
- },
719
- shouldContinue: options.shouldContinue,
720
- signal: options.signal,
721
- });
722
- } finally {
723
- this._eventBus.emit('ai:workflow:done', {
724
- workflow: name,
725
- params,
726
- totalSteps: finalStep?.step || 0,
727
- result: finalStep?.type || 'unknown',
728
- }, { appName: 'wu-ai' });
729
- }
730
- }
731
-
732
- /**
733
- * Execute workflow deterministically — NO AI NEEDED.
734
- * Steps are exact actions: { action: 'click', selector: '#btn' }
735
- * @private
736
- */
737
- async *_executeDeterministic(name, workflow, params, options) {
738
- // Interpolate parameters into step values
739
- const steps = workflow.steps.map(step => {
740
- const interpolated = { ...step };
741
- for (const [key, value] of Object.entries(params)) {
742
- const pattern = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
743
- for (const field of ['value', 'selector', 'text', 'section', 'event', 'path']) {
744
- if (typeof interpolated[field] === 'string') {
745
- interpolated[field] = interpolated[field].replace(pattern, String(value));
746
- }
747
- }
748
- }
749
- return interpolated;
750
- });
751
-
752
- this._eventBus?.emit('ai:workflow:start', {
753
- workflow: name,
754
- mode: 'deterministic',
755
- params,
756
- steps: steps.length,
757
- }, { appName: 'wu-ai' });
758
-
759
- let lastStep = null;
760
-
761
- try {
762
- for (let i = 0; i < steps.length; i++) {
763
- const step = steps[i];
764
- const stepNum = i + 1;
765
- const startTime = Date.now();
766
-
767
- // Check abort
768
- if (options.signal?.aborted) {
769
- const result = {
770
- step: stepNum,
771
- type: 'aborted',
772
- content: 'Workflow aborted',
773
- reason: 'Aborted by caller',
774
- elapsed: 0,
775
- };
776
- lastStep = result;
777
- yield result;
778
- return;
779
- }
780
-
781
- // Handle 'wait' specially — it's async
782
- if (step.action === 'wait') {
783
- if (step.selector) {
784
- const found = await waitForSelector(step.selector, step.timeout || 5000);
785
- const elapsed = Date.now() - startTime;
786
- const result = {
787
- step: stepNum,
788
- type: found ? 'action' : 'error',
789
- content: found
790
- ? `Waited for "${step.selector}" — found`
791
- : `Timeout waiting for "${step.selector}"`,
792
- elapsed,
793
- };
794
- lastStep = result;
795
- if (options.onStep) options.onStep(result);
796
- yield result;
797
- if (!found) return; // stop on timeout
798
- } else if (step.ms) {
799
- await delay(step.ms);
800
- const result = {
801
- step: stepNum,
802
- type: 'action',
803
- content: `Waited ${step.ms}ms`,
804
- elapsed: step.ms,
805
- };
806
- lastStep = result;
807
- if (options.onStep) options.onStep(result);
808
- yield result;
809
- }
810
- continue;
811
- }
812
-
813
- // Execute the step
814
- const execResult = executeDeterministicStep(step, this._eventBus, this._store);
815
- const elapsed = Date.now() - startTime;
816
-
817
- const stepResult = {
818
- step: stepNum,
819
- type: execResult.success ? 'action' : 'error',
820
- content: execResult.success ? execResult.detail : execResult.error,
821
- elapsed,
822
- };
823
-
824
- lastStep = stepResult;
825
- if (options.onStep) options.onStep(stepResult);
826
- yield stepResult;
827
-
828
- // Human-in-the-loop gate
829
- if (options.shouldContinue) {
830
- let shouldGo;
831
- try {
832
- shouldGo = await options.shouldContinue(stepResult);
833
- } catch {
834
- shouldGo = false;
835
- }
836
- if (!shouldGo) {
837
- const interrupted = {
838
- step: stepNum,
839
- type: 'interrupted',
840
- content: 'Stopped by user',
841
- reason: 'shouldContinue returned false',
842
- elapsed: 0,
843
- };
844
- lastStep = interrupted;
845
- yield interrupted;
846
- return;
847
- }
848
- }
849
-
850
- // Stop on error
851
- if (!execResult.success) return;
852
-
853
- // Small delay between steps for UI to update
854
- if (i < steps.length - 1) {
855
- await delay(step.delay ?? 200);
856
- }
857
- }
858
-
859
- // All steps completed
860
- const done = {
861
- step: steps.length,
862
- type: 'done',
863
- content: `Workflow "${name}" completed (${steps.length} steps)`,
864
- reason: 'All steps executed',
865
- elapsed: 0,
866
- };
867
- lastStep = done;
868
- if (options.onStep) options.onStep(done);
869
- yield done;
870
-
871
- } finally {
872
- this._eventBus?.emit('ai:workflow:done', {
873
- workflow: name,
874
- mode: 'deterministic',
875
- params,
876
- totalSteps: lastStep?.step || 0,
877
- result: lastStep?.type || 'unknown',
878
- }, { appName: 'wu-ai' });
879
- }
880
- }
881
-
882
- /**
883
- * Check if a workflow is registered.
884
- *
885
- * @param {string} name
886
- * @returns {boolean}
887
- */
888
- hasWorkflow(name) {
889
- return this._workflows.has(name);
890
- }
891
-
892
- /**
893
- * Get a workflow definition.
894
- *
895
- * @param {string} name
896
- * @returns {object|null}
897
- */
898
- getWorkflow(name) {
899
- const w = this._workflows.get(name);
900
- if (!w) return null;
901
- return {
902
- description: w.description,
903
- steps: [...w.steps],
904
- mode: w.mode,
905
- parameters: { ...w.parameters },
906
- maxSteps: w.maxSteps,
907
- };
908
- }
909
-
910
- /**
911
- * Remove a registered workflow.
912
- *
913
- * @param {string} name
914
- */
915
- removeWorkflow(name) {
916
- this._workflows.delete(name);
917
- }
918
-
919
- /**
920
- * Get all workflow names.
921
- *
922
- * @returns {string[]}
923
- */
924
- getWorkflowNames() {
925
- return [...this._workflows.keys()];
926
- }
927
-
928
- /** @private */
929
- _buildWorkflowGoal(workflow, steps, params) {
930
- const parts = [];
931
-
932
- parts.push(
933
- `WORKFLOW: ${workflow.description}`,
934
- '',
935
- 'You must follow these steps IN ORDER. Use browser tools (screenshot, click, type)',
936
- 'to interact with the application. After each step, take a screenshot to verify.',
937
- '',
938
- 'STEPS:',
939
- );
940
-
941
- for (let i = 0; i < steps.length; i++) {
942
- parts.push(` ${i + 1}. ${steps[i]}`);
943
- }
944
-
945
- parts.push(
946
- '',
947
- 'After completing all steps successfully, respond with [DONE].',
948
- 'If a step fails, explain what went wrong.',
949
- );
950
-
951
- // Add capability context if available
952
- const capMap = this.getCapabilityMap();
953
- const appNames = Object.keys(capMap);
954
- if (appNames.length > 0) {
955
- parts.push('', 'AVAILABLE APP CAPABILITIES:');
956
- for (const appName of appNames) {
957
- for (const cap of capMap[appName]) {
958
- parts.push(` - ${cap.action}: ${cap.description}`);
959
- }
960
- }
961
- }
962
-
963
- return parts.join('\n');
964
- }
965
-
966
- // ─── Stats & Lifecycle ──────────────────────────────────────────
967
-
968
- getStats() {
969
- return {
970
- ...this._stats,
971
- registeredApps: this.getRegisteredApps(),
972
- totalCapabilities: this.getTotalCapabilities(),
973
- capabilityMap: this.getCapabilityMap(),
974
- workflows: this.getWorkflowNames(),
975
- config: { ...this._config },
976
- };
977
- }
978
-
979
- destroy() {
980
- // Remove all capabilities from the action registry
981
- for (const [appName, actions] of this._capabilities) {
982
- for (const [actionName] of actions) {
983
- this._actions.unregister(`${appName}:${actionName}`);
984
- }
985
- }
986
- this._capabilities.clear();
987
- this._workflows.clear();
988
-
989
- this._stats = {
990
- totalIntents: 0,
991
- resolvedIntents: 0,
992
- failedIntents: 0,
993
- workflowsRegistered: 0,
994
- workflowsExecuted: 0,
995
- };
996
- }
997
-
998
- // ─── Private Helpers ────────────────────────────────────────────
999
-
1000
- /**
1001
- * Extract app names from tool results based on qualified action names.
1002
- * e.g., tool name 'orders:getRecent' → app 'orders'
1003
- */
1004
- _extractInvolvedApps(toolResults) {
1005
- if (!toolResults || !Array.isArray(toolResults)) return [];
1006
- const apps = new Set();
1007
- for (const result of toolResults) {
1008
- const name = result.name || result.tool || '';
1009
- const colonIdx = name.indexOf(':');
1010
- if (colonIdx > 0) {
1011
- apps.add(name.slice(0, colonIdx));
1012
- }
1013
- }
1014
- return [...apps];
1015
- }
1016
-
1017
- _generateNamespace() {
1018
- return INTENT_NAMESPACE_PREFIX + Date.now().toString(36) +
1019
- '_' + Math.random().toString(36).slice(2, 6);
1020
- }
1021
- }
1
+ /**
2
+ * WU-AI-ORCHESTRATE: Cross-Micro-App AI Coordination (Paradigm 4)
3
+ *
4
+ * The fourth paradigm of wu.ai:
5
+ * Paradigm 1 — App sends messages to LLM (conversation)
6
+ * Paradigm 2 — External LLM calls into app (tools/WebMCP)
7
+ * Paradigm 3 — AI as autonomous director (agent loop)
8
+ * Paradigm 4 — AI as microfrontend glue (this file)
9
+ *
10
+ * The Problem:
11
+ * In microfrontend architectures, apps are isolated by design.
12
+ * Cross-app coordination requires manual wiring through events
13
+ * and shared state. As apps grow, wiring becomes n² complexity.
14
+ *
15
+ * The Solution:
16
+ * Each micro-app declares its capabilities to the AI layer.
17
+ * The AI understands the semantic meaning of each capability
18
+ * and can resolve natural-language intents by calling the right
19
+ * actions across the right apps — without tight coupling.
20
+ *
21
+ * Key Concepts:
22
+ * - Capability: An action scoped to a specific micro-app
23
+ * Registered as 'appName:actionName', cleaned up on unmount.
24
+ * - Intent: A natural-language cross-app request resolved in
25
+ * a single conversation turn with an orchestrator system prompt.
26
+ * - Capability Map: The AI's understanding of system topology —
27
+ * which apps exist and what each can do.
28
+ *
29
+ * This module does NOT replace actions, triggers, or agents.
30
+ * It enriches them with cross-app topology awareness.
31
+ *
32
+ * API (accessible via wu.ai):
33
+ * wu.ai.capability(app, name, config) → Register app-scoped capability
34
+ * wu.ai.intent(description, options) → Resolve cross-app intent
35
+ * wu.ai.removeApp(appName) → Cleanup on unmount
36
+ * wu.ai.workflow(name, config) → Register reusable AI workflow
37
+ * wu.ai.runWorkflow(name, params, opts) → Execute a registered workflow
38
+ */
39
+
40
+ import { logger } from '../core/wu-logger.js';
41
+ import { clickElement, typeIntoElement } from './wu-ai-browser-primitives.js';
42
+
43
+ // ─── Constants ──────────────────────────────────────────────────
44
+
45
+ const INTENT_NAMESPACE_PREFIX = 'intent:';
46
+
47
+ // ─── Deterministic Step Actions ─────────────────────────────────
48
+
49
+ /**
50
+ * Execute a single deterministic workflow step.
51
+ * No AI needed — directly calls browser primitives.
52
+ *
53
+ * @param {object} step - Step definition
54
+ * @param {string} step.action - 'click' | 'type' | 'navigate' | 'wait' | 'emit' | 'setState'
55
+ * @param {object} params - Interpolated params
56
+ * @returns {{ success: boolean, detail?: string, error?: string }}
57
+ */
58
+ function executeDeterministicStep(step, eventBus, store) {
59
+ switch (step.action) {
60
+ case 'click': {
61
+ const result = clickElement(step.selector, step.text);
62
+ if (result.error) return { success: false, error: result.error };
63
+ return { success: true, detail: `Clicked: ${step.selector || step.text}` };
64
+ }
65
+
66
+ case 'type': {
67
+ const result = typeIntoElement(step.selector, step.value, {
68
+ clear: step.clear ?? true,
69
+ submit: step.submit ?? false,
70
+ });
71
+ if (result.error) return { success: false, error: result.error };
72
+ return { success: true, detail: `Typed "${step.value}" into ${step.selector}` };
73
+ }
74
+
75
+ case 'navigate': {
76
+ if (eventBus && step.section) {
77
+ eventBus.emit('nav:section', { section: step.section }, { appName: 'wu-ai' });
78
+ return { success: true, detail: `Navigated to section: ${step.section}` };
79
+ }
80
+ if (step.selector) {
81
+ const result = clickElement(step.selector, step.text);
82
+ if (result.error) return { success: false, error: result.error };
83
+ return { success: true, detail: `Navigated via click: ${step.selector}` };
84
+ }
85
+ return { success: false, error: 'navigate requires "section" or "selector"' };
86
+ }
87
+
88
+ case 'wait': {
89
+ // Wait is handled by the runner (delay + optional selector poll)
90
+ return { success: true, detail: `Wait: ${step.ms || 0}ms` };
91
+ }
92
+
93
+ case 'emit': {
94
+ if (!eventBus) return { success: false, error: 'eventBus not available' };
95
+ eventBus.emit(step.event, step.data || {}, { appName: 'wu-ai' });
96
+ return { success: true, detail: `Emitted: ${step.event}` };
97
+ }
98
+
99
+ case 'setState': {
100
+ if (!store) return { success: false, error: 'store not available' };
101
+ store.set(step.path, step.value);
102
+ return { success: true, detail: `Set state: ${step.path}` };
103
+ }
104
+
105
+ default:
106
+ return { success: false, error: `Unknown action: ${step.action}` };
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Wait for a selector to appear in the DOM (with timeout).
112
+ *
113
+ * @param {string} selector
114
+ * @param {number} timeout - ms
115
+ * @returns {Promise<boolean>}
116
+ */
117
+ function waitForSelector(selector, timeout = 5000) {
118
+ return new Promise((resolve) => {
119
+ if (document.querySelector(selector)) {
120
+ resolve(true);
121
+ return;
122
+ }
123
+
124
+ const interval = 100;
125
+ let elapsed = 0;
126
+ const timer = setInterval(() => {
127
+ elapsed += interval;
128
+ if (document.querySelector(selector)) {
129
+ clearInterval(timer);
130
+ resolve(true);
131
+ } else if (elapsed >= timeout) {
132
+ clearInterval(timer);
133
+ resolve(false);
134
+ }
135
+ }, interval);
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Simple delay.
141
+ */
142
+ function delay(ms) {
143
+ return new Promise((resolve) => setTimeout(resolve, ms));
144
+ }
145
+
146
+ // ─── WuAIOrchestrate ────────────────────────────────────────────
147
+
148
+ export class WuAIOrchestrate {
149
+ /**
150
+ * @param {object} deps
151
+ * @param {import('./wu-ai-actions.js').WuAIActions} deps.actions
152
+ * @param {import('./wu-ai-conversation.js').WuAIConversation} deps.conversation
153
+ * @param {import('./wu-ai-context.js').WuAIContext} deps.context
154
+ * @param {import('./wu-ai-permissions.js').WuAIPermissions} deps.permissions
155
+ * @param {object} deps.eventBus
156
+ */
157
+ constructor({ actions, conversation, context, permissions, eventBus, agent, store }) {
158
+ this._actions = actions;
159
+ this._conversation = conversation;
160
+ this._context = context;
161
+ this._permissions = permissions;
162
+ this._eventBus = eventBus;
163
+ this._agent = agent; // WuAIAgent — for workflow execution
164
+ this._store = store; // WuStore — for deterministic setState steps
165
+
166
+ // appName → Map<actionName, { description, qualifiedName }>
167
+ this._capabilities = new Map();
168
+
169
+ // name → { goal, steps, parameters, provider, ... }
170
+ this._workflows = new Map();
171
+
172
+ this._config = {
173
+ defaultProvider: null,
174
+ defaultTemperature: 0.3, // lower temp for orchestration
175
+ };
176
+
177
+ this._stats = {
178
+ totalIntents: 0,
179
+ resolvedIntents: 0,
180
+ failedIntents: 0,
181
+ workflowsRegistered: 0,
182
+ workflowsExecuted: 0,
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Post-init configuration.
188
+ *
189
+ * @param {object} config
190
+ * @param {string} [config.defaultProvider] - Default provider for intents
191
+ * @param {number} [config.defaultTemperature] - Default temperature for intents
192
+ */
193
+ configure(config) {
194
+ if (config.defaultProvider !== undefined) this._config.defaultProvider = config.defaultProvider;
195
+ if (config.defaultTemperature !== undefined) this._config.defaultTemperature = config.defaultTemperature;
196
+ }
197
+
198
+ // ─── Capability Registration ────────────────────────────────────
199
+
200
+ /**
201
+ * Register a capability scoped to a micro-app.
202
+ *
203
+ * Under the hood this registers a normal action with the qualified
204
+ * name 'appName:actionName' so the LLM can call it directly.
205
+ * The capability map is tracked separately for lifecycle management
206
+ * (removeApp) and system prompt enrichment.
207
+ *
208
+ * @param {string} appName - The micro-app name (e.g., 'orders', 'dashboard')
209
+ * @param {string} actionName - The capability name (e.g., 'getRecent', 'updateKPIs')
210
+ * @param {object} config - Same as wu.ai.action() config:
211
+ * { description, parameters, handler, confirm?, permissions?, dangerous? }
212
+ */
213
+ register(appName, actionName, config) {
214
+ if (!appName || !actionName) {
215
+ throw new Error('[wu-ai] capability() requires both appName and actionName');
216
+ }
217
+ if (!config || typeof config.handler !== 'function') {
218
+ throw new Error(`[wu-ai] capability '${appName}:${actionName}' must have a handler function`);
219
+ }
220
+
221
+ const qualifiedName = `${appName}:${actionName}`;
222
+
223
+ // Track in capability map
224
+ if (!this._capabilities.has(appName)) {
225
+ this._capabilities.set(appName, new Map());
226
+ }
227
+ this._capabilities.get(appName).set(actionName, {
228
+ description: config.description || actionName,
229
+ qualifiedName,
230
+ });
231
+
232
+ // Register as a normal action with enriched description
233
+ this._actions.register(qualifiedName, {
234
+ ...config,
235
+ description: `[${appName}] ${config.description || actionName}`,
236
+ });
237
+
238
+ logger.wuDebug(`[wu-ai] Capability registered: ${qualifiedName}`);
239
+ }
240
+
241
+ /**
242
+ * Remove a single capability.
243
+ *
244
+ * @param {string} appName
245
+ * @param {string} actionName
246
+ */
247
+ unregister(appName, actionName) {
248
+ const appCaps = this._capabilities.get(appName);
249
+ if (!appCaps) return;
250
+
251
+ const qualifiedName = `${appName}:${actionName}`;
252
+ appCaps.delete(actionName);
253
+ this._actions.unregister(qualifiedName);
254
+
255
+ // Clean up empty app entry
256
+ if (appCaps.size === 0) {
257
+ this._capabilities.delete(appName);
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Remove all capabilities for a micro-app (unmount cleanup).
263
+ *
264
+ * Call this when a micro-app is unmounted to prevent stale
265
+ * capabilities from appearing in the AI's capability map.
266
+ *
267
+ * @param {string} appName
268
+ * @returns {number} Number of capabilities removed
269
+ */
270
+ removeApp(appName) {
271
+ const appCaps = this._capabilities.get(appName);
272
+ if (!appCaps) return 0;
273
+
274
+ let removed = 0;
275
+ for (const [actionName] of appCaps) {
276
+ const qualifiedName = `${appName}:${actionName}`;
277
+ this._actions.unregister(qualifiedName);
278
+ removed++;
279
+ }
280
+
281
+ this._capabilities.delete(appName);
282
+
283
+ logger.wuDebug(`[wu-ai] All capabilities removed for app '${appName}' (${removed})`);
284
+
285
+ this._eventBus.emit('ai:app:removed', {
286
+ appName,
287
+ capabilitiesRemoved: removed,
288
+ }, { appName: 'wu-ai' });
289
+
290
+ return removed;
291
+ }
292
+
293
+ // ─── Capability Map ─────────────────────────────────────────────
294
+
295
+ /**
296
+ * Get the full capability map grouped by app.
297
+ *
298
+ * Used for system prompt enrichment and debugging.
299
+ *
300
+ * @returns {object} { appName: [{ action, description }], ... }
301
+ */
302
+ getCapabilityMap() {
303
+ const map = {};
304
+ for (const [appName, actions] of this._capabilities) {
305
+ map[appName] = [];
306
+ for (const [, meta] of actions) {
307
+ map[appName].push({
308
+ action: meta.qualifiedName,
309
+ description: meta.description,
310
+ });
311
+ }
312
+ }
313
+ return map;
314
+ }
315
+
316
+ /**
317
+ * Get app names that have registered capabilities.
318
+ *
319
+ * @returns {string[]}
320
+ */
321
+ getRegisteredApps() {
322
+ return [...this._capabilities.keys()];
323
+ }
324
+
325
+ /**
326
+ * Check if an app has registered capabilities.
327
+ *
328
+ * @param {string} appName
329
+ * @returns {boolean}
330
+ */
331
+ hasApp(appName) {
332
+ return this._capabilities.has(appName);
333
+ }
334
+
335
+ /**
336
+ * Get the total number of registered capabilities across all apps.
337
+ *
338
+ * @returns {number}
339
+ */
340
+ getTotalCapabilities() {
341
+ let count = 0;
342
+ for (const actions of this._capabilities.values()) {
343
+ count += actions.size;
344
+ }
345
+ return count;
346
+ }
347
+
348
+ // ─── Intent Resolution ──────────────────────────────────────────
349
+
350
+ /**
351
+ * Resolve a cross-app intent in a single conversation turn.
352
+ *
353
+ * The AI receives:
354
+ * - The full capability map (what each app can do)
355
+ * - Current application state (via context)
356
+ * - Mounted apps list
357
+ * - All registered tools (capabilities are tools)
358
+ *
359
+ * Unlike agent(), this is NOT a multi-step autonomous loop.
360
+ * The LLM resolves the intent in one logical request (which may
361
+ * include multiple tool calls within the conversation's tool-call
362
+ * loop, but conceptually is a single turn).
363
+ *
364
+ * Unlike send(), the namespace is ephemeral and auto-cleaned,
365
+ * and the system prompt is auto-built with the capability map.
366
+ *
367
+ * @param {string} description - Natural language intent
368
+ * e.g., "Show me the top customer by order count"
369
+ * e.g., "Update dashboard stats and notify the topbar"
370
+ * @param {object} [options]
371
+ * @param {string[]} [options.plan] - Optional action sequence hint.
372
+ * The AI uses this as guidance but can deviate if needed.
373
+ * e.g., ['orders:getRecent', 'customers:lookup']
374
+ * @param {string} [options.provider] - LLM provider override
375
+ * @param {number} [options.temperature] - Temperature override
376
+ * @param {number} [options.maxTokens] - Max tokens override
377
+ * @param {AbortSignal} [options.signal] - Abort signal
378
+ * @param {string|object} [options.responseFormat] - Response format
379
+ * @returns {Promise<{
380
+ * content: string,
381
+ * tool_results: Array,
382
+ * usage: object|null,
383
+ * resolved: boolean,
384
+ * appsInvolved: string[]
385
+ * }>}
386
+ */
387
+ async resolve(description, options = {}) {
388
+ if (!description || typeof description !== 'string') {
389
+ throw new Error('[wu-ai] intent() requires a description string');
390
+ }
391
+
392
+ this._stats.totalIntents++;
393
+ const namespace = this._generateNamespace();
394
+
395
+ // Collect fresh context before building the prompt
396
+ if (this._context) {
397
+ try {
398
+ await this._context.collect();
399
+ } catch {
400
+ // Context collection is best-effort
401
+ }
402
+ }
403
+
404
+ const systemPrompt = this._buildOrchestratorPrompt(options);
405
+
406
+ this._eventBus.emit('ai:intent:start', {
407
+ description: description.slice(0, 200),
408
+ namespace,
409
+ capabilities: this.getTotalCapabilities(),
410
+ }, { appName: 'wu-ai' });
411
+
412
+ try {
413
+ const response = await this._conversation.send(description, {
414
+ namespace,
415
+ systemPrompt,
416
+ provider: options.provider || this._config.defaultProvider,
417
+ temperature: options.temperature ?? this._config.defaultTemperature,
418
+ maxTokens: options.maxTokens,
419
+ signal: options.signal,
420
+ responseFormat: options.responseFormat,
421
+ });
422
+
423
+ const toolResults = response.tool_results || [];
424
+ const appsInvolved = this._extractInvolvedApps(toolResults);
425
+ const resolved = !!(response.content);
426
+
427
+ if (resolved) {
428
+ this._stats.resolvedIntents++;
429
+ } else {
430
+ this._stats.failedIntents++;
431
+ }
432
+
433
+ const result = {
434
+ content: response.content || '',
435
+ tool_results: toolResults,
436
+ usage: response.usage || null,
437
+ resolved,
438
+ appsInvolved,
439
+ };
440
+
441
+ this._eventBus.emit('ai:intent:resolved', {
442
+ description: description.slice(0, 200),
443
+ resolved,
444
+ appsInvolved,
445
+ }, { appName: 'wu-ai' });
446
+
447
+ return result;
448
+ } catch (err) {
449
+ this._stats.failedIntents++;
450
+
451
+ this._eventBus.emit('ai:intent:error', {
452
+ description: description.slice(0, 200),
453
+ error: err.message,
454
+ }, { appName: 'wu-ai' });
455
+
456
+ throw err;
457
+ } finally {
458
+ // Always clean up the ephemeral namespace
459
+ this._conversation.deleteNamespace(namespace);
460
+ }
461
+ }
462
+
463
+ // ─── System Prompt Builder ──────────────────────────────────────
464
+
465
+ /**
466
+ * Build the orchestrator system prompt with full capability map.
467
+ *
468
+ * This method is also available to other modules (triggers, agents)
469
+ * that want capability-aware system prompts.
470
+ *
471
+ * @param {object} [options]
472
+ * @param {string[]} [options.plan] - Optional action sequence hint
473
+ * @returns {string}
474
+ */
475
+ buildOrchestratorPrompt(options = {}) {
476
+ return this._buildOrchestratorPrompt(options);
477
+ }
478
+
479
+ /** @private */
480
+ _buildOrchestratorPrompt(options = {}) {
481
+ const parts = [];
482
+
483
+ parts.push(
484
+ 'You are an AI orchestrator for a microfrontend application.',
485
+ 'Multiple independent apps are mounted, each with specific capabilities.',
486
+ 'Resolve cross-app requests by calling the right capabilities in the right order.',
487
+ '',
488
+ 'RULES:',
489
+ '- Call capabilities (tools) to gather data or trigger actions.',
490
+ '- You may call multiple capabilities from different apps if needed.',
491
+ '- Synthesize results into a clear, actionable response.',
492
+ '- If a required app is not available or lacks a capability, explain what is missing.',
493
+ '',
494
+ );
495
+
496
+ // Capability map
497
+ const capMap = this.getCapabilityMap();
498
+ const appNames = Object.keys(capMap);
499
+
500
+ if (appNames.length > 0) {
501
+ parts.push('CAPABILITY MAP:');
502
+ for (const appName of appNames) {
503
+ parts.push(` ${appName}:`);
504
+ for (const cap of capMap[appName]) {
505
+ parts.push(` - ${cap.action}: ${cap.description}`);
506
+ }
507
+ }
508
+ parts.push('');
509
+ } else {
510
+ parts.push(
511
+ 'NOTE: No app capabilities are registered. Answer based on available context only.',
512
+ '',
513
+ );
514
+ }
515
+
516
+ // Optional plan hint
517
+ if (options.plan && options.plan.length > 0) {
518
+ parts.push(
519
+ 'SUGGESTED PLAN (follow this unless a better approach is evident):',
520
+ );
521
+ for (let i = 0; i < options.plan.length; i++) {
522
+ parts.push(` ${i + 1}. ${options.plan[i]}`);
523
+ }
524
+ parts.push('');
525
+ }
526
+
527
+ // Context snapshot (state, mounted apps)
528
+ const snapshot = this._context?.getSnapshot();
529
+ if (snapshot?._mountedApps?.length) {
530
+ parts.push(`MOUNTED APPS: ${snapshot._mountedApps.join(', ')}`, '');
531
+ }
532
+ if (snapshot?._store && Object.keys(snapshot._store).length > 0) {
533
+ parts.push(
534
+ 'CURRENT STATE:',
535
+ JSON.stringify(snapshot._store, null, 2),
536
+ '',
537
+ );
538
+ }
539
+
540
+ return parts.join('\n');
541
+ }
542
+
543
+ // ─── Workflows ─────────────────────────────────────────────────
544
+
545
+ /**
546
+ * Register a reusable AI workflow.
547
+ *
548
+ * A workflow is a named, parameterized recipe that the AI agent
549
+ * follows step by step. Think of it as a macro: you define it once,
550
+ * then run it whenever you need with different parameters.
551
+ *
552
+ * The AI receives the steps as instructions and uses browser actions
553
+ * (screenshot, click, type) plus any registered capabilities/actions
554
+ * to execute them. You can watch every step in real time.
555
+ *
556
+ * @param {string} name - Workflow name (e.g., 'register-user')
557
+ * @param {object} config
558
+ * @param {string} config.description - What this workflow does
559
+ * @param {string[]} config.steps - Step-by-step instructions for the AI
560
+ * @param {object} [config.parameters] - Parameter definitions for interpolation
561
+ * e.g., { name: { type: 'string', required: true }, email: { type: 'string' } }
562
+ * @param {number} [config.maxSteps=15] - Max agent steps allowed
563
+ * @param {string} [config.provider] - LLM provider to use
564
+ * @param {number} [config.temperature] - Temperature (default: 0.2 for precision)
565
+ *
566
+ * @example
567
+ * // ── AI Mode (default): steps are natural language ──
568
+ * wu.ai.workflow('register-user', {
569
+ * description: 'Register a new user in the system',
570
+ * steps: [
571
+ * 'Navigate to the Customers section',
572
+ * 'Click the "Add Customer" button',
573
+ * 'Fill in the name field with {{name}}',
574
+ * 'Click Submit',
575
+ * ],
576
+ * parameters: { name: { type: 'string', required: true } },
577
+ * });
578
+ *
579
+ * // ── Deterministic Mode: steps are exact actions, NO AI NEEDED ──
580
+ * wu.ai.workflow('register-user', {
581
+ * mode: 'deterministic',
582
+ * description: 'Register a new user',
583
+ * steps: [
584
+ * { action: 'navigate', section: 'customers' },
585
+ * { action: 'click', selector: '#add-customer-btn' },
586
+ * { action: 'type', selector: '#name', value: '{{name}}' },
587
+ * { action: 'type', selector: '#email', value: '{{email}}' },
588
+ * { action: 'click', selector: '#submit-btn' },
589
+ * { action: 'wait', selector: '.success-message', timeout: 5000 },
590
+ * ],
591
+ * parameters: {
592
+ * name: { type: 'string', required: true },
593
+ * email: { type: 'string', required: true },
594
+ * },
595
+ * });
596
+ */
597
+ registerWorkflow(name, config) {
598
+ if (!name) {
599
+ throw new Error('[wu-ai] workflow() requires a name');
600
+ }
601
+ if (!config || !config.steps || !Array.isArray(config.steps) || config.steps.length === 0) {
602
+ throw new Error(`[wu-ai] workflow '${name}' must have a non-empty steps array`);
603
+ }
604
+
605
+ // Detect mode: if steps are objects with 'action', it's deterministic
606
+ const mode = config.mode || (
607
+ config.steps.length > 0 && typeof config.steps[0] === 'object' && config.steps[0].action
608
+ ? 'deterministic'
609
+ : 'ai'
610
+ );
611
+
612
+ this._workflows.set(name, {
613
+ description: config.description || name,
614
+ steps: config.steps,
615
+ mode,
616
+ parameters: config.parameters || {},
617
+ maxSteps: config.maxSteps ?? 15,
618
+ provider: config.provider || null,
619
+ temperature: config.temperature ?? 0.2,
620
+ });
621
+
622
+ this._stats.workflowsRegistered++;
623
+
624
+ logger.wuDebug(`[wu-ai] Workflow registered: '${name}' (${config.steps.length} steps)`);
625
+ }
626
+
627
+ /**
628
+ * Execute a registered workflow with parameters.
629
+ *
630
+ * Returns an async generator (like agent) — you iterate over it
631
+ * to observe each step in real time.
632
+ *
633
+ * @param {string} name - Workflow name
634
+ * @param {object} [params={}] - Parameters to interpolate into steps
635
+ * e.g., { name: 'Juan Pérez', email: 'juan@test.com' }
636
+ * @param {object} [options={}]
637
+ * @param {Function} [options.onStep] - Callback per step
638
+ * @param {Function} [options.shouldContinue] - Human-in-the-loop gate
639
+ * @param {AbortSignal} [options.signal] - Abort signal
640
+ * @returns {AsyncGenerator<AgentStepResult>}
641
+ *
642
+ * @example
643
+ * for await (const step of wu.ai.runWorkflow('register-user', {
644
+ * name: 'Juan Pérez',
645
+ * email: 'juan@test.com',
646
+ * })) {
647
+ * console.log(`Paso ${step.step}: ${step.content}`);
648
+ * if (step.type === 'done') console.log('Workflow completado!');
649
+ * }
650
+ *
651
+ * // With human approval per step:
652
+ * for await (const step of wu.ai.runWorkflow('register-user', params, {
653
+ * shouldContinue: (step) => confirm(`¿Continuar? ${step.content?.slice(0, 60)}`),
654
+ * })) {
655
+ * renderStep(step);
656
+ * }
657
+ */
658
+ async *executeWorkflow(name, params = {}, options = {}) {
659
+ const workflow = this._workflows.get(name);
660
+ if (!workflow) {
661
+ throw new Error(`[wu-ai] Workflow '${name}' is not registered`);
662
+ }
663
+
664
+ // Validate required parameters
665
+ for (const [paramName, paramConfig] of Object.entries(workflow.parameters)) {
666
+ if (paramConfig.required && (params[paramName] === undefined || params[paramName] === null)) {
667
+ throw new Error(`[wu-ai] Workflow '${name}' requires parameter '${paramName}'`);
668
+ }
669
+ }
670
+
671
+ this._stats.workflowsExecuted++;
672
+
673
+ // Branch on mode
674
+ if (workflow.mode === 'deterministic') {
675
+ yield* this._executeDeterministic(name, workflow, params, options);
676
+ } else {
677
+ yield* this._executeWithAgent(name, workflow, params, options);
678
+ }
679
+ }
680
+
681
+ /**
682
+ * Execute workflow using the AI agent (natural language steps).
683
+ * @private
684
+ */
685
+ async *_executeWithAgent(name, workflow, params, options) {
686
+ if (!this._agent) {
687
+ throw new Error('[wu-ai] Agent module not available for workflow execution');
688
+ }
689
+
690
+ // Interpolate parameters into string steps
691
+ const interpolatedSteps = workflow.steps.map(step => {
692
+ let result = step;
693
+ for (const [key, value] of Object.entries(params)) {
694
+ result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), String(value));
695
+ }
696
+ return result;
697
+ });
698
+
699
+ const goal = this._buildWorkflowGoal(workflow, interpolatedSteps, params);
700
+
701
+ this._eventBus.emit('ai:workflow:start', {
702
+ workflow: name,
703
+ mode: 'ai',
704
+ params,
705
+ steps: interpolatedSteps.length,
706
+ }, { appName: 'wu-ai' });
707
+
708
+ let finalStep = null;
709
+
710
+ try {
711
+ yield* this._agent.run(goal, {
712
+ maxSteps: workflow.maxSteps,
713
+ provider: options.provider || workflow.provider || this._config.defaultProvider,
714
+ temperature: workflow.temperature ?? 0.2,
715
+ onStep: (step) => {
716
+ finalStep = step;
717
+ if (options.onStep) options.onStep(step);
718
+ },
719
+ shouldContinue: options.shouldContinue,
720
+ signal: options.signal,
721
+ });
722
+ } finally {
723
+ this._eventBus.emit('ai:workflow:done', {
724
+ workflow: name,
725
+ params,
726
+ totalSteps: finalStep?.step || 0,
727
+ result: finalStep?.type || 'unknown',
728
+ }, { appName: 'wu-ai' });
729
+ }
730
+ }
731
+
732
+ /**
733
+ * Execute workflow deterministically — NO AI NEEDED.
734
+ * Steps are exact actions: { action: 'click', selector: '#btn' }
735
+ * @private
736
+ */
737
+ async *_executeDeterministic(name, workflow, params, options) {
738
+ // Interpolate parameters into step values
739
+ const steps = workflow.steps.map(step => {
740
+ const interpolated = { ...step };
741
+ for (const [key, value] of Object.entries(params)) {
742
+ const pattern = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
743
+ for (const field of ['value', 'selector', 'text', 'section', 'event', 'path']) {
744
+ if (typeof interpolated[field] === 'string') {
745
+ interpolated[field] = interpolated[field].replace(pattern, String(value));
746
+ }
747
+ }
748
+ }
749
+ return interpolated;
750
+ });
751
+
752
+ this._eventBus?.emit('ai:workflow:start', {
753
+ workflow: name,
754
+ mode: 'deterministic',
755
+ params,
756
+ steps: steps.length,
757
+ }, { appName: 'wu-ai' });
758
+
759
+ let lastStep = null;
760
+
761
+ try {
762
+ for (let i = 0; i < steps.length; i++) {
763
+ const step = steps[i];
764
+ const stepNum = i + 1;
765
+ const startTime = Date.now();
766
+
767
+ // Check abort
768
+ if (options.signal?.aborted) {
769
+ const result = {
770
+ step: stepNum,
771
+ type: 'aborted',
772
+ content: 'Workflow aborted',
773
+ reason: 'Aborted by caller',
774
+ elapsed: 0,
775
+ };
776
+ lastStep = result;
777
+ yield result;
778
+ return;
779
+ }
780
+
781
+ // Handle 'wait' specially — it's async
782
+ if (step.action === 'wait') {
783
+ if (step.selector) {
784
+ const found = await waitForSelector(step.selector, step.timeout || 5000);
785
+ const elapsed = Date.now() - startTime;
786
+ const result = {
787
+ step: stepNum,
788
+ type: found ? 'action' : 'error',
789
+ content: found
790
+ ? `Waited for "${step.selector}" — found`
791
+ : `Timeout waiting for "${step.selector}"`,
792
+ elapsed,
793
+ };
794
+ lastStep = result;
795
+ if (options.onStep) options.onStep(result);
796
+ yield result;
797
+ if (!found) return; // stop on timeout
798
+ } else if (step.ms) {
799
+ await delay(step.ms);
800
+ const result = {
801
+ step: stepNum,
802
+ type: 'action',
803
+ content: `Waited ${step.ms}ms`,
804
+ elapsed: step.ms,
805
+ };
806
+ lastStep = result;
807
+ if (options.onStep) options.onStep(result);
808
+ yield result;
809
+ }
810
+ continue;
811
+ }
812
+
813
+ // Execute the step
814
+ const execResult = executeDeterministicStep(step, this._eventBus, this._store);
815
+ const elapsed = Date.now() - startTime;
816
+
817
+ const stepResult = {
818
+ step: stepNum,
819
+ type: execResult.success ? 'action' : 'error',
820
+ content: execResult.success ? execResult.detail : execResult.error,
821
+ elapsed,
822
+ };
823
+
824
+ lastStep = stepResult;
825
+ if (options.onStep) options.onStep(stepResult);
826
+ yield stepResult;
827
+
828
+ // Human-in-the-loop gate
829
+ if (options.shouldContinue) {
830
+ let shouldGo;
831
+ try {
832
+ shouldGo = await options.shouldContinue(stepResult);
833
+ } catch {
834
+ shouldGo = false;
835
+ }
836
+ if (!shouldGo) {
837
+ const interrupted = {
838
+ step: stepNum,
839
+ type: 'interrupted',
840
+ content: 'Stopped by user',
841
+ reason: 'shouldContinue returned false',
842
+ elapsed: 0,
843
+ };
844
+ lastStep = interrupted;
845
+ yield interrupted;
846
+ return;
847
+ }
848
+ }
849
+
850
+ // Stop on error
851
+ if (!execResult.success) return;
852
+
853
+ // Small delay between steps for UI to update
854
+ if (i < steps.length - 1) {
855
+ await delay(step.delay ?? 200);
856
+ }
857
+ }
858
+
859
+ // All steps completed
860
+ const done = {
861
+ step: steps.length,
862
+ type: 'done',
863
+ content: `Workflow "${name}" completed (${steps.length} steps)`,
864
+ reason: 'All steps executed',
865
+ elapsed: 0,
866
+ };
867
+ lastStep = done;
868
+ if (options.onStep) options.onStep(done);
869
+ yield done;
870
+
871
+ } finally {
872
+ this._eventBus?.emit('ai:workflow:done', {
873
+ workflow: name,
874
+ mode: 'deterministic',
875
+ params,
876
+ totalSteps: lastStep?.step || 0,
877
+ result: lastStep?.type || 'unknown',
878
+ }, { appName: 'wu-ai' });
879
+ }
880
+ }
881
+
882
+ /**
883
+ * Check if a workflow is registered.
884
+ *
885
+ * @param {string} name
886
+ * @returns {boolean}
887
+ */
888
+ hasWorkflow(name) {
889
+ return this._workflows.has(name);
890
+ }
891
+
892
+ /**
893
+ * Get a workflow definition.
894
+ *
895
+ * @param {string} name
896
+ * @returns {object|null}
897
+ */
898
+ getWorkflow(name) {
899
+ const w = this._workflows.get(name);
900
+ if (!w) return null;
901
+ return {
902
+ description: w.description,
903
+ steps: [...w.steps],
904
+ mode: w.mode,
905
+ parameters: { ...w.parameters },
906
+ maxSteps: w.maxSteps,
907
+ };
908
+ }
909
+
910
+ /**
911
+ * Remove a registered workflow.
912
+ *
913
+ * @param {string} name
914
+ */
915
+ removeWorkflow(name) {
916
+ this._workflows.delete(name);
917
+ }
918
+
919
+ /**
920
+ * Get all workflow names.
921
+ *
922
+ * @returns {string[]}
923
+ */
924
+ getWorkflowNames() {
925
+ return [...this._workflows.keys()];
926
+ }
927
+
928
+ /** @private */
929
+ _buildWorkflowGoal(workflow, steps, params) {
930
+ const parts = [];
931
+
932
+ parts.push(
933
+ `WORKFLOW: ${workflow.description}`,
934
+ '',
935
+ 'You must follow these steps IN ORDER. Use browser tools (screenshot, click, type)',
936
+ 'to interact with the application. After each step, take a screenshot to verify.',
937
+ '',
938
+ 'STEPS:',
939
+ );
940
+
941
+ for (let i = 0; i < steps.length; i++) {
942
+ parts.push(` ${i + 1}. ${steps[i]}`);
943
+ }
944
+
945
+ parts.push(
946
+ '',
947
+ 'After completing all steps successfully, respond with [DONE].',
948
+ 'If a step fails, explain what went wrong.',
949
+ );
950
+
951
+ // Add capability context if available
952
+ const capMap = this.getCapabilityMap();
953
+ const appNames = Object.keys(capMap);
954
+ if (appNames.length > 0) {
955
+ parts.push('', 'AVAILABLE APP CAPABILITIES:');
956
+ for (const appName of appNames) {
957
+ for (const cap of capMap[appName]) {
958
+ parts.push(` - ${cap.action}: ${cap.description}`);
959
+ }
960
+ }
961
+ }
962
+
963
+ return parts.join('\n');
964
+ }
965
+
966
+ // ─── Stats & Lifecycle ──────────────────────────────────────────
967
+
968
+ getStats() {
969
+ return {
970
+ ...this._stats,
971
+ registeredApps: this.getRegisteredApps(),
972
+ totalCapabilities: this.getTotalCapabilities(),
973
+ capabilityMap: this.getCapabilityMap(),
974
+ workflows: this.getWorkflowNames(),
975
+ config: { ...this._config },
976
+ };
977
+ }
978
+
979
+ destroy() {
980
+ // Remove all capabilities from the action registry
981
+ for (const [appName, actions] of this._capabilities) {
982
+ for (const [actionName] of actions) {
983
+ this._actions.unregister(`${appName}:${actionName}`);
984
+ }
985
+ }
986
+ this._capabilities.clear();
987
+ this._workflows.clear();
988
+
989
+ this._stats = {
990
+ totalIntents: 0,
991
+ resolvedIntents: 0,
992
+ failedIntents: 0,
993
+ workflowsRegistered: 0,
994
+ workflowsExecuted: 0,
995
+ };
996
+ }
997
+
998
+ // ─── Private Helpers ────────────────────────────────────────────
999
+
1000
+ /**
1001
+ * Extract app names from tool results based on qualified action names.
1002
+ * e.g., tool name 'orders:getRecent' → app 'orders'
1003
+ */
1004
+ _extractInvolvedApps(toolResults) {
1005
+ if (!toolResults || !Array.isArray(toolResults)) return [];
1006
+ const apps = new Set();
1007
+ for (const result of toolResults) {
1008
+ const name = result.name || result.tool || '';
1009
+ const colonIdx = name.indexOf(':');
1010
+ if (colonIdx > 0) {
1011
+ apps.add(name.slice(0, colonIdx));
1012
+ }
1013
+ }
1014
+ return [...apps];
1015
+ }
1016
+
1017
+ _generateNamespace() {
1018
+ return INTENT_NAMESPACE_PREFIX + Date.now().toString(36) +
1019
+ '_' + Math.random().toString(36).slice(2, 6);
1020
+ }
1021
+ }