wu-framework 1.1.6 → 1.1.8
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 +511 -977
- package/dist/wu-framework.cjs.js +3 -1
- package/dist/wu-framework.cjs.js.map +1 -0
- package/dist/wu-framework.dev.js +7533 -2761
- package/dist/wu-framework.dev.js.map +1 -1
- package/dist/wu-framework.esm.js +3 -0
- package/dist/wu-framework.esm.js.map +1 -0
- package/dist/wu-framework.umd.js +3 -1
- package/dist/wu-framework.umd.js.map +1 -0
- package/integrations/astro/README.md +127 -0
- package/integrations/astro/WuApp.astro +63 -0
- package/integrations/astro/WuShell.astro +39 -0
- package/integrations/astro/index.js +68 -0
- package/integrations/astro/package.json +38 -0
- package/integrations/astro/types.d.ts +53 -0
- package/package.json +94 -74
- package/src/adapters/angular/ai.js +30 -0
- package/src/adapters/angular/index.d.ts +154 -0
- package/src/adapters/angular/index.js +932 -0
- package/src/adapters/angular.d.ts +3 -154
- package/src/adapters/angular.js +3 -813
- package/src/adapters/index.js +35 -24
- package/src/adapters/lit/ai.js +20 -0
- package/src/adapters/lit/index.d.ts +120 -0
- package/src/adapters/lit/index.js +721 -0
- package/src/adapters/lit.d.ts +3 -120
- package/src/adapters/lit.js +3 -726
- package/src/adapters/preact/ai.js +33 -0
- package/src/adapters/preact/index.d.ts +108 -0
- package/src/adapters/preact/index.js +661 -0
- package/src/adapters/preact.d.ts +3 -108
- package/src/adapters/preact.js +3 -665
- package/src/adapters/react/ai.js +135 -0
- package/src/adapters/react/index.d.ts +246 -0
- package/src/adapters/react/index.js +689 -0
- package/src/adapters/react.d.ts +3 -212
- package/src/adapters/react.js +3 -513
- package/src/adapters/shared.js +64 -0
- package/src/adapters/solid/ai.js +32 -0
- package/src/adapters/solid/index.d.ts +101 -0
- package/src/adapters/solid/index.js +586 -0
- package/src/adapters/solid.d.ts +3 -101
- package/src/adapters/solid.js +3 -591
- package/src/adapters/svelte/ai.js +31 -0
- package/src/adapters/svelte/index.d.ts +166 -0
- package/src/adapters/svelte/index.js +798 -0
- package/src/adapters/svelte.d.ts +3 -166
- package/src/adapters/svelte.js +3 -803
- package/src/adapters/vanilla/ai.js +30 -0
- package/src/adapters/vanilla/index.d.ts +179 -0
- package/src/adapters/vanilla/index.js +785 -0
- package/src/adapters/vanilla.d.ts +3 -179
- package/src/adapters/vanilla.js +3 -791
- package/src/adapters/vue/ai.js +52 -0
- package/src/adapters/vue/index.d.ts +299 -0
- package/src/adapters/vue/index.js +608 -0
- package/src/adapters/vue.d.ts +3 -299
- package/src/adapters/vue.js +3 -611
- package/src/ai/wu-ai-actions.js +261 -0
- package/src/ai/wu-ai-browser.js +663 -0
- package/src/ai/wu-ai-context.js +332 -0
- package/src/ai/wu-ai-conversation.js +554 -0
- package/src/ai/wu-ai-permissions.js +381 -0
- package/src/ai/wu-ai-provider.js +605 -0
- package/src/ai/wu-ai-schema.js +225 -0
- package/src/ai/wu-ai-triggers.js +396 -0
- package/src/ai/wu-ai.js +474 -0
- package/src/core/wu-app.js +50 -8
- package/src/core/wu-cache.js +1 -1
- package/src/core/wu-core.js +645 -677
- package/src/core/wu-html-parser.js +121 -211
- package/src/core/wu-iframe-sandbox.js +328 -0
- package/src/core/wu-mcp-bridge.js +647 -0
- package/src/core/wu-overrides.js +510 -0
- package/src/core/wu-prefetch.js +414 -0
- package/src/core/wu-proxy-sandbox.js +398 -75
- package/src/core/wu-sandbox.js +86 -268
- package/src/core/wu-script-executor.js +79 -182
- package/src/core/wu-snapshot-sandbox.js +149 -106
- package/src/core/wu-strategies.js +13 -0
- package/src/core/wu-style-bridge.js +0 -2
- package/src/index.js +139 -665
- package/dist/wu-framework.hex.js +0 -23
- package/dist/wu-framework.min.js +0 -1
- package/dist/wu-framework.obf.js +0 -1
- package/scripts/build-protected.js +0 -366
- package/scripts/build.js +0 -212
- package/scripts/rollup-plugin-hex.js +0 -143
- package/src/core/wu-registry.js +0 -60
- package/src/core/wu-sandbox-pool.js +0 -390
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WU-AI-CONVERSATION: Multi-turn conversation manager
|
|
3
|
+
*
|
|
4
|
+
* Manages conversation state per namespace. Each namespace maintains
|
|
5
|
+
* its own message history, enabling multiple independent AI conversations
|
|
6
|
+
* (e.g., chat widget, background triggers, admin panel).
|
|
7
|
+
*
|
|
8
|
+
* Key features:
|
|
9
|
+
* - Multi-turn per namespace (isolated histories)
|
|
10
|
+
* - Streaming with async generator passthrough
|
|
11
|
+
* - Tool call loop (max rounds configurable, default 5)
|
|
12
|
+
* - Abort support via AbortController
|
|
13
|
+
* - Automatic context injection before each send
|
|
14
|
+
* - Token-aware history truncation
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { logger } from '../core/wu-logger.js';
|
|
18
|
+
import { sanitizeForPrompt, interpolate } from './wu-ai-schema.js';
|
|
19
|
+
|
|
20
|
+
// ─── Default Config ──────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const DEFAULT_CONFIG = {
|
|
23
|
+
maxHistoryMessages: 50, // per namespace
|
|
24
|
+
maxToolRounds: 5, // tool call loop limit
|
|
25
|
+
defaultNamespace: 'default',
|
|
26
|
+
systemPrompt: null, // string or function returning string
|
|
27
|
+
temperature: undefined,
|
|
28
|
+
maxTokens: undefined,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ─── Conversation Namespace ──────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
class ConversationNamespace {
|
|
34
|
+
constructor(name) {
|
|
35
|
+
this.name = name;
|
|
36
|
+
this.messages = [];
|
|
37
|
+
this.createdAt = Date.now();
|
|
38
|
+
this.lastActivity = Date.now();
|
|
39
|
+
this._abortController = null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
addMessage(msg) {
|
|
43
|
+
this.messages.push({ ...msg, _ts: Date.now() });
|
|
44
|
+
this.lastActivity = Date.now();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getMessages() {
|
|
48
|
+
return this.messages.map(({ _ts, ...rest }) => rest);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
truncate(maxMessages) {
|
|
52
|
+
if (this.messages.length <= maxMessages) return;
|
|
53
|
+
|
|
54
|
+
// Keep system messages + last N messages
|
|
55
|
+
const system = this.messages.filter(m => m.role === 'system');
|
|
56
|
+
const nonSystem = this.messages.filter(m => m.role !== 'system');
|
|
57
|
+
const kept = nonSystem.slice(-maxMessages);
|
|
58
|
+
this.messages = [...system, ...kept];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
clear() {
|
|
62
|
+
this.messages = [];
|
|
63
|
+
this.lastActivity = Date.now();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
abort() {
|
|
67
|
+
if (this._abortController) {
|
|
68
|
+
this._abortController.abort();
|
|
69
|
+
this._abortController = null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
createAbortController() {
|
|
74
|
+
this.abort(); // cancel previous if any
|
|
75
|
+
this._abortController = new AbortController();
|
|
76
|
+
return this._abortController;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── Main Conversation Manager ───────────────────────────────────
|
|
81
|
+
|
|
82
|
+
export class WuAIConversation {
|
|
83
|
+
constructor({ provider, actions, context, permissions, eventBus }) {
|
|
84
|
+
this._provider = provider;
|
|
85
|
+
this._actions = actions;
|
|
86
|
+
this._context = context;
|
|
87
|
+
this._permissions = permissions;
|
|
88
|
+
this._eventBus = eventBus;
|
|
89
|
+
|
|
90
|
+
this._config = { ...DEFAULT_CONFIG };
|
|
91
|
+
this._namespaces = new Map();
|
|
92
|
+
this._activeRequests = new Map(); // namespace → promise
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Configure conversation defaults.
|
|
97
|
+
*/
|
|
98
|
+
configure(config) {
|
|
99
|
+
Object.assign(this._config, config);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Send a message and get a complete response.
|
|
104
|
+
* Handles multi-turn tool call loops automatically.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} message - User message
|
|
107
|
+
* @param {object} [options]
|
|
108
|
+
* @param {string} [options.namespace='default'] - Conversation namespace
|
|
109
|
+
* @param {string} [options.systemPrompt] - Override system prompt
|
|
110
|
+
* @param {object} [options.templateVars] - Variables for template interpolation
|
|
111
|
+
* @param {number} [options.temperature] - Override temperature
|
|
112
|
+
* @param {number} [options.maxTokens] - Override max tokens
|
|
113
|
+
* @param {AbortSignal} [options.signal] - External abort signal
|
|
114
|
+
* @returns {Promise<{ content: string, tool_results?: Array, usage?: object, namespace: string }>}
|
|
115
|
+
*/
|
|
116
|
+
async send(message, options = {}) {
|
|
117
|
+
const ns = this._getOrCreateNamespace(options.namespace);
|
|
118
|
+
const traceId = this._permissions.loopProtection.createTraceId();
|
|
119
|
+
const meta = { namespace: ns.name, depth: 0, traceId };
|
|
120
|
+
|
|
121
|
+
// Preflight checks
|
|
122
|
+
const preflight = this._permissions.preflight(meta);
|
|
123
|
+
if (!preflight.allowed) {
|
|
124
|
+
return { content: `[blocked] ${preflight.reason}`, namespace: ns.name };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Build system prompt
|
|
128
|
+
const systemPrompt = await this._buildSystemPrompt(options);
|
|
129
|
+
|
|
130
|
+
// Ensure system message is set/updated
|
|
131
|
+
this._setSystemMessage(ns, systemPrompt);
|
|
132
|
+
|
|
133
|
+
// Add user message
|
|
134
|
+
const processedMessage = this._processMessage(message, options.templateVars);
|
|
135
|
+
ns.addMessage({ role: 'user', content: processedMessage });
|
|
136
|
+
|
|
137
|
+
// Truncate history if needed
|
|
138
|
+
ns.truncate(this._config.maxHistoryMessages);
|
|
139
|
+
|
|
140
|
+
// Create abort controller (merges with external signal)
|
|
141
|
+
const controller = ns.createAbortController();
|
|
142
|
+
const signal = this._mergeSignals(controller.signal, options.signal);
|
|
143
|
+
|
|
144
|
+
// Rate limit tracking
|
|
145
|
+
this._permissions.rateLimiter.recordStart(ns.name);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
// Tool call loop
|
|
149
|
+
const toolResults = [];
|
|
150
|
+
let rounds = 0;
|
|
151
|
+
const maxRounds = this._config.maxToolRounds;
|
|
152
|
+
|
|
153
|
+
while (rounds <= maxRounds) {
|
|
154
|
+
// Get tools if actions are registered
|
|
155
|
+
const tools = this._actions.getToolSchemas();
|
|
156
|
+
|
|
157
|
+
const response = await this._provider.send(ns.getMessages(), {
|
|
158
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
159
|
+
temperature: options.temperature ?? this._config.temperature,
|
|
160
|
+
maxTokens: options.maxTokens ?? this._config.maxTokens,
|
|
161
|
+
signal,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Add assistant response to history
|
|
165
|
+
const assistantMsg = { role: 'assistant', content: response.content || '' };
|
|
166
|
+
if (response.tool_calls) assistantMsg.tool_calls = response.tool_calls;
|
|
167
|
+
ns.addMessage(assistantMsg);
|
|
168
|
+
|
|
169
|
+
// No tool calls → we're done
|
|
170
|
+
if (!response.tool_calls || response.tool_calls.length === 0) {
|
|
171
|
+
this._permissions.circuitBreaker.recordSuccess();
|
|
172
|
+
|
|
173
|
+
this._eventBus.emit('ai:response', {
|
|
174
|
+
namespace: ns.name,
|
|
175
|
+
content: response.content,
|
|
176
|
+
toolResults: toolResults.length > 0 ? toolResults : undefined,
|
|
177
|
+
usage: response.usage,
|
|
178
|
+
traceId,
|
|
179
|
+
}, { appName: 'wu-ai' });
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
content: response.content || '',
|
|
183
|
+
tool_results: toolResults.length > 0 ? toolResults : undefined,
|
|
184
|
+
usage: response.usage,
|
|
185
|
+
namespace: ns.name,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Execute tool calls
|
|
190
|
+
rounds++;
|
|
191
|
+
if (rounds > maxRounds) {
|
|
192
|
+
const msg = `[wu-ai] Tool call loop limit (${maxRounds}) reached in namespace '${ns.name}'`;
|
|
193
|
+
logger.wuWarn(msg);
|
|
194
|
+
ns.addMessage({ role: 'assistant', content: msg });
|
|
195
|
+
return { content: msg, tool_results: toolResults, namespace: ns.name };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Loop protection
|
|
199
|
+
this._permissions.loopProtection.enter(traceId);
|
|
200
|
+
|
|
201
|
+
for (const toolCall of response.tool_calls) {
|
|
202
|
+
const result = await this._actions.execute(toolCall.name, toolCall.arguments, {
|
|
203
|
+
traceId,
|
|
204
|
+
depth: rounds,
|
|
205
|
+
callId: toolCall.id,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
toolResults.push({
|
|
209
|
+
tool: toolCall.name,
|
|
210
|
+
params: toolCall.arguments,
|
|
211
|
+
result: result.result,
|
|
212
|
+
success: result.success,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Add tool result to conversation
|
|
216
|
+
ns.addMessage({
|
|
217
|
+
role: 'tool',
|
|
218
|
+
content: JSON.stringify(result.success ? result.result : { error: result.reason }),
|
|
219
|
+
tool_call_id: toolCall.id,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
this._permissions.loopProtection.exit(traceId);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Should not reach here, but safety net
|
|
227
|
+
return { content: '', tool_results: toolResults, namespace: ns.name };
|
|
228
|
+
|
|
229
|
+
} catch (err) {
|
|
230
|
+
this._permissions.circuitBreaker.recordFailure();
|
|
231
|
+
|
|
232
|
+
if (err.name === 'AbortError') {
|
|
233
|
+
return { content: '[aborted]', namespace: ns.name };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
this._eventBus.emit('ai:error', {
|
|
237
|
+
namespace: ns.name,
|
|
238
|
+
error: err.message,
|
|
239
|
+
traceId,
|
|
240
|
+
}, { appName: 'wu-ai' });
|
|
241
|
+
|
|
242
|
+
throw err;
|
|
243
|
+
} finally {
|
|
244
|
+
this._permissions.rateLimiter.recordEnd();
|
|
245
|
+
ns._abortController = null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Send a message with streaming response.
|
|
251
|
+
* Returns an async generator that yields chunks.
|
|
252
|
+
*
|
|
253
|
+
* @param {string} message - User message
|
|
254
|
+
* @param {object} [options] - Same as send()
|
|
255
|
+
* @yields {{ type: 'text'|'tool_call'|'done'|'error', content?: string, tool_call?: object }}
|
|
256
|
+
*/
|
|
257
|
+
async *stream(message, options = {}) {
|
|
258
|
+
const ns = this._getOrCreateNamespace(options.namespace);
|
|
259
|
+
const traceId = this._permissions.loopProtection.createTraceId();
|
|
260
|
+
const meta = { namespace: ns.name, depth: 0, traceId };
|
|
261
|
+
|
|
262
|
+
// Preflight checks
|
|
263
|
+
const preflight = this._permissions.preflight(meta);
|
|
264
|
+
if (!preflight.allowed) {
|
|
265
|
+
yield { type: 'error', error: preflight.reason };
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Build system prompt and set it
|
|
270
|
+
const systemPrompt = await this._buildSystemPrompt(options);
|
|
271
|
+
this._setSystemMessage(ns, systemPrompt);
|
|
272
|
+
|
|
273
|
+
// Add user message
|
|
274
|
+
const processedMessage = this._processMessage(message, options.templateVars);
|
|
275
|
+
ns.addMessage({ role: 'user', content: processedMessage });
|
|
276
|
+
ns.truncate(this._config.maxHistoryMessages);
|
|
277
|
+
|
|
278
|
+
// Abort controller
|
|
279
|
+
const controller = ns.createAbortController();
|
|
280
|
+
const signal = this._mergeSignals(controller.signal, options.signal);
|
|
281
|
+
|
|
282
|
+
this._permissions.rateLimiter.recordStart(ns.name);
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const tools = this._actions.getToolSchemas();
|
|
286
|
+
let fullContent = '';
|
|
287
|
+
|
|
288
|
+
// Accumulator for streaming tool call deltas
|
|
289
|
+
const toolCallAccumulator = new Map(); // index → { id, name, args }
|
|
290
|
+
|
|
291
|
+
for await (const chunk of this._provider.stream(ns.getMessages(), {
|
|
292
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
293
|
+
temperature: options.temperature ?? this._config.temperature,
|
|
294
|
+
maxTokens: options.maxTokens ?? this._config.maxTokens,
|
|
295
|
+
signal,
|
|
296
|
+
})) {
|
|
297
|
+
if (chunk.type === 'text') {
|
|
298
|
+
fullContent += chunk.content;
|
|
299
|
+
yield chunk;
|
|
300
|
+
} else if (chunk.type === 'tool_call_start') {
|
|
301
|
+
// Start accumulating a new tool call
|
|
302
|
+
toolCallAccumulator.set(toolCallAccumulator.size, {
|
|
303
|
+
id: chunk.id,
|
|
304
|
+
name: chunk.name,
|
|
305
|
+
args: '',
|
|
306
|
+
});
|
|
307
|
+
} else if (chunk.type === 'tool_call_delta') {
|
|
308
|
+
// Accumulate arguments delta
|
|
309
|
+
const idx = chunk.index ?? (toolCallAccumulator.size - 1);
|
|
310
|
+
const acc = toolCallAccumulator.get(idx);
|
|
311
|
+
if (acc) {
|
|
312
|
+
if (chunk.id) acc.id = chunk.id;
|
|
313
|
+
if (chunk.name) acc.name = chunk.name;
|
|
314
|
+
acc.args += chunk.argumentsDelta || '';
|
|
315
|
+
} else {
|
|
316
|
+
// New tool call via OpenAI format
|
|
317
|
+
toolCallAccumulator.set(idx, {
|
|
318
|
+
id: chunk.id || `tc_${idx}`,
|
|
319
|
+
name: chunk.name || '',
|
|
320
|
+
args: chunk.argumentsDelta || '',
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
} else if (chunk.type === 'done') {
|
|
324
|
+
// If we accumulated tool calls, execute them
|
|
325
|
+
if (toolCallAccumulator.size > 0) {
|
|
326
|
+
const toolCalls = [];
|
|
327
|
+
for (const [, acc] of toolCallAccumulator) {
|
|
328
|
+
let parsedArgs = {};
|
|
329
|
+
try { parsedArgs = JSON.parse(acc.args); } catch { /* empty args */ }
|
|
330
|
+
toolCalls.push({ id: acc.id, name: acc.name, arguments: parsedArgs });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Add assistant message with tool calls
|
|
334
|
+
ns.addMessage({ role: 'assistant', content: fullContent, tool_calls: toolCalls });
|
|
335
|
+
|
|
336
|
+
// Execute each tool call
|
|
337
|
+
this._permissions.loopProtection.enter(traceId);
|
|
338
|
+
for (const tc of toolCalls) {
|
|
339
|
+
const result = await this._actions.execute(tc.name, tc.arguments, {
|
|
340
|
+
traceId, depth: 1, callId: tc.id,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
yield {
|
|
344
|
+
type: 'tool_result',
|
|
345
|
+
tool: tc.name,
|
|
346
|
+
result: result.success ? result.result : { error: result.reason },
|
|
347
|
+
success: result.success,
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
ns.addMessage({
|
|
351
|
+
role: 'tool',
|
|
352
|
+
content: JSON.stringify(result.success ? result.result : { error: result.reason }),
|
|
353
|
+
tool_call_id: tc.id,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
this._permissions.loopProtection.exit(traceId);
|
|
357
|
+
|
|
358
|
+
// After tool execution in streaming, yield done — caller can re-stream if needed
|
|
359
|
+
yield { type: 'tool_calls_done', count: toolCalls.length };
|
|
360
|
+
} else {
|
|
361
|
+
// No tool calls — add assistant message and finish
|
|
362
|
+
ns.addMessage({ role: 'assistant', content: fullContent });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
this._permissions.circuitBreaker.recordSuccess();
|
|
366
|
+
yield { type: 'done' };
|
|
367
|
+
return;
|
|
368
|
+
} else if (chunk.type === 'usage') {
|
|
369
|
+
yield chunk;
|
|
370
|
+
} else if (chunk.type === 'error') {
|
|
371
|
+
yield chunk;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Stream ended without explicit 'done' chunk
|
|
376
|
+
if (fullContent) {
|
|
377
|
+
ns.addMessage({ role: 'assistant', content: fullContent });
|
|
378
|
+
}
|
|
379
|
+
this._permissions.circuitBreaker.recordSuccess();
|
|
380
|
+
yield { type: 'done' };
|
|
381
|
+
|
|
382
|
+
} catch (err) {
|
|
383
|
+
this._permissions.circuitBreaker.recordFailure();
|
|
384
|
+
|
|
385
|
+
if (err.name === 'AbortError') {
|
|
386
|
+
yield { type: 'error', error: 'aborted' };
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
yield { type: 'error', error: err.message };
|
|
391
|
+
|
|
392
|
+
this._eventBus.emit('ai:error', {
|
|
393
|
+
namespace: ns.name,
|
|
394
|
+
error: err.message,
|
|
395
|
+
traceId,
|
|
396
|
+
}, { appName: 'wu-ai' });
|
|
397
|
+
} finally {
|
|
398
|
+
this._permissions.rateLimiter.recordEnd();
|
|
399
|
+
ns._abortController = null;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Add a message to a namespace without sending to LLM.
|
|
405
|
+
* Useful for injecting context or tool results manually.
|
|
406
|
+
*/
|
|
407
|
+
inject(role, content, options = {}) {
|
|
408
|
+
const ns = this._getOrCreateNamespace(options.namespace);
|
|
409
|
+
ns.addMessage({ role, content });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Get conversation history for a namespace.
|
|
414
|
+
*/
|
|
415
|
+
getHistory(namespace) {
|
|
416
|
+
const ns = this._namespaces.get(namespace || this._config.defaultNamespace);
|
|
417
|
+
return ns ? ns.getMessages() : [];
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Clear conversation history for a namespace.
|
|
422
|
+
*/
|
|
423
|
+
clear(namespace) {
|
|
424
|
+
const ns = this._namespaces.get(namespace || this._config.defaultNamespace);
|
|
425
|
+
if (ns) ns.clear();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Clear all namespaces.
|
|
430
|
+
*/
|
|
431
|
+
clearAll() {
|
|
432
|
+
for (const ns of this._namespaces.values()) {
|
|
433
|
+
ns.clear();
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Abort an active request in a namespace.
|
|
439
|
+
*/
|
|
440
|
+
abort(namespace) {
|
|
441
|
+
const ns = this._namespaces.get(namespace || this._config.defaultNamespace);
|
|
442
|
+
if (ns) ns.abort();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Abort all active requests.
|
|
447
|
+
*/
|
|
448
|
+
abortAll() {
|
|
449
|
+
for (const ns of this._namespaces.values()) {
|
|
450
|
+
ns.abort();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* List active namespaces.
|
|
456
|
+
*/
|
|
457
|
+
getNamespaces() {
|
|
458
|
+
return [...this._namespaces.keys()];
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Delete a namespace entirely.
|
|
463
|
+
*/
|
|
464
|
+
deleteNamespace(namespace) {
|
|
465
|
+
const ns = this._namespaces.get(namespace);
|
|
466
|
+
if (ns) {
|
|
467
|
+
ns.abort();
|
|
468
|
+
this._namespaces.delete(namespace);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ── Private ──
|
|
473
|
+
|
|
474
|
+
_getOrCreateNamespace(name) {
|
|
475
|
+
const nsName = name || this._config.defaultNamespace;
|
|
476
|
+
if (!this._namespaces.has(nsName)) {
|
|
477
|
+
this._namespaces.set(nsName, new ConversationNamespace(nsName));
|
|
478
|
+
}
|
|
479
|
+
return this._namespaces.get(nsName);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async _buildSystemPrompt(options) {
|
|
483
|
+
// 1. Explicit override
|
|
484
|
+
if (options.systemPrompt) {
|
|
485
|
+
return typeof options.systemPrompt === 'function'
|
|
486
|
+
? await options.systemPrompt()
|
|
487
|
+
: options.systemPrompt;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// 2. Config-level system prompt
|
|
491
|
+
if (this._config.systemPrompt) {
|
|
492
|
+
return typeof this._config.systemPrompt === 'function'
|
|
493
|
+
? await this._config.systemPrompt()
|
|
494
|
+
: this._config.systemPrompt;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// 3. Auto-generate from context
|
|
498
|
+
if (this._context) {
|
|
499
|
+
await this._context.collect();
|
|
500
|
+
const tools = this._actions.getToolSchemas();
|
|
501
|
+
return this._context.toSystemPrompt({ tools });
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return 'You are an AI assistant connected to a web application via Wu Framework.';
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
_setSystemMessage(ns, systemPrompt) {
|
|
508
|
+
// Replace or insert system message at the beginning
|
|
509
|
+
if (ns.messages.length > 0 && ns.messages[0].role === 'system') {
|
|
510
|
+
ns.messages[0].content = systemPrompt;
|
|
511
|
+
ns.messages[0]._ts = Date.now();
|
|
512
|
+
} else {
|
|
513
|
+
ns.messages.unshift({ role: 'system', content: systemPrompt, _ts: Date.now() });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
_processMessage(message, templateVars) {
|
|
518
|
+
if (!templateVars) return message;
|
|
519
|
+
try {
|
|
520
|
+
const contextVars = this._context?.getInterpolationContext() || {};
|
|
521
|
+
return interpolate(message, { ...contextVars, ...templateVars });
|
|
522
|
+
} catch {
|
|
523
|
+
return message;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
_mergeSignals(internalSignal, externalSignal) {
|
|
528
|
+
if (!externalSignal) return internalSignal;
|
|
529
|
+
|
|
530
|
+
// If AbortSignal.any is available (modern browsers), use it
|
|
531
|
+
if (typeof AbortSignal !== 'undefined' && AbortSignal.any) {
|
|
532
|
+
return AbortSignal.any([internalSignal, externalSignal]);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Fallback: create a new controller that listens to both
|
|
536
|
+
const merged = new AbortController();
|
|
537
|
+
const onAbort = () => merged.abort();
|
|
538
|
+
internalSignal.addEventListener('abort', onAbort, { once: true });
|
|
539
|
+
externalSignal.addEventListener('abort', onAbort, { once: true });
|
|
540
|
+
return merged.signal;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
getStats() {
|
|
544
|
+
const namespaces = {};
|
|
545
|
+
for (const [name, ns] of this._namespaces) {
|
|
546
|
+
namespaces[name] = {
|
|
547
|
+
messageCount: ns.messages.length,
|
|
548
|
+
lastActivity: ns.lastActivity,
|
|
549
|
+
hasActiveRequest: !!ns._abortController,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
return { namespaces, config: { ...this._config } };
|
|
553
|
+
}
|
|
554
|
+
}
|