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.
- package/README.md +52 -20
- package/dist/wu-framework.cjs.js +1 -1
- package/dist/wu-framework.cjs.js.map +1 -1
- package/dist/wu-framework.dev.js +15511 -15146
- package/dist/wu-framework.dev.js.map +1 -1
- package/dist/wu-framework.esm.js +1 -1
- package/dist/wu-framework.esm.js.map +1 -1
- package/dist/wu-framework.umd.js +1 -1
- package/dist/wu-framework.umd.js.map +1 -1
- package/package.json +166 -161
- package/src/adapters/angular/ai.js +30 -30
- package/src/adapters/angular/index.d.ts +154 -154
- package/src/adapters/angular/index.js +932 -932
- package/src/adapters/angular.d.ts +3 -3
- package/src/adapters/angular.js +3 -3
- package/src/adapters/index.js +168 -168
- package/src/adapters/lit/ai.js +20 -20
- package/src/adapters/lit/index.d.ts +120 -120
- package/src/adapters/lit/index.js +721 -721
- package/src/adapters/lit.d.ts +3 -3
- package/src/adapters/lit.js +3 -3
- package/src/adapters/preact/ai.js +33 -33
- package/src/adapters/preact/index.d.ts +108 -108
- package/src/adapters/preact/index.js +661 -661
- package/src/adapters/preact.d.ts +3 -3
- package/src/adapters/preact.js +3 -3
- package/src/adapters/react/index.js +48 -54
- package/src/adapters/react.d.ts +3 -3
- package/src/adapters/react.js +3 -3
- package/src/adapters/shared.js +64 -64
- package/src/adapters/solid/ai.js +32 -32
- package/src/adapters/solid/index.d.ts +101 -101
- package/src/adapters/solid/index.js +586 -586
- package/src/adapters/solid.d.ts +3 -3
- package/src/adapters/solid.js +3 -3
- package/src/adapters/svelte/ai.js +31 -31
- package/src/adapters/svelte/index.d.ts +166 -166
- package/src/adapters/svelte/index.js +798 -798
- package/src/adapters/svelte.d.ts +3 -3
- package/src/adapters/svelte.js +3 -3
- package/src/adapters/vanilla/ai.js +30 -30
- package/src/adapters/vanilla/index.d.ts +179 -179
- package/src/adapters/vanilla/index.js +785 -785
- package/src/adapters/vanilla.d.ts +3 -3
- package/src/adapters/vanilla.js +3 -3
- package/src/adapters/vue/ai.js +52 -52
- package/src/adapters/vue/index.d.ts +299 -299
- package/src/adapters/vue/index.js +610 -610
- package/src/adapters/vue.d.ts +3 -3
- package/src/adapters/vue.js +3 -3
- package/src/ai/wu-ai-actions.js +261 -261
- package/src/ai/wu-ai-agent.js +546 -546
- package/src/ai/wu-ai-browser-primitives.js +354 -354
- package/src/ai/wu-ai-browser.js +380 -380
- package/src/ai/wu-ai-context.js +332 -332
- package/src/ai/wu-ai-conversation.js +613 -613
- package/src/ai/wu-ai-orchestrate.js +1021 -1021
- package/src/ai/wu-ai-permissions.js +381 -381
- package/src/ai/wu-ai-provider.js +700 -700
- package/src/ai/wu-ai-schema.js +225 -225
- package/src/ai/wu-ai-triggers.js +396 -396
- package/src/ai/wu-ai.js +804 -804
- package/src/core/wu-app.js +236 -236
- package/src/core/wu-cache.js +498 -477
- package/src/core/wu-core.js +1412 -1398
- package/src/core/wu-error-boundary.js +396 -382
- package/src/core/wu-event-bus.js +390 -348
- package/src/core/wu-hooks.js +350 -350
- package/src/core/wu-html-parser.js +199 -190
- package/src/core/wu-iframe-sandbox.js +328 -328
- package/src/core/wu-loader.js +385 -273
- package/src/core/wu-logger.js +142 -134
- package/src/core/wu-manifest.js +532 -509
- package/src/core/wu-mcp-bridge.js +432 -432
- package/src/core/wu-overrides.js +510 -510
- package/src/core/wu-performance.js +228 -228
- package/src/core/wu-plugin.js +401 -348
- package/src/core/wu-prefetch.js +414 -414
- package/src/core/wu-proxy-sandbox.js +477 -476
- package/src/core/wu-sandbox.js +779 -779
- package/src/core/wu-script-executor.js +161 -113
- package/src/core/wu-snapshot-sandbox.js +227 -227
- package/src/core/wu-store.js +13 -3
- package/src/core/wu-strategies.js +256 -256
- package/src/core/wu-style-bridge.js +477 -477
- package/src/index.d.ts +317 -0
- package/src/index.js +234 -224
- 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
|
+
}
|