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
package/src/ai/wu-ai-agent.js
CHANGED
|
@@ -1,546 +1,546 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WU-AI-AGENT: Autonomous agent loop (Paradigm 3)
|
|
3
|
-
*
|
|
4
|
-
* The agent is the third 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 (this file)
|
|
8
|
-
*
|
|
9
|
-
* The agent receives a goal and iterates: send to LLM, execute tool calls,
|
|
10
|
-
* observe results, repeat — until the goal is achieved or limits are hit.
|
|
11
|
-
* It is an async generator, yielding each step so the caller can observe,
|
|
12
|
-
* log, render, or intervene at any point.
|
|
13
|
-
*
|
|
14
|
-
* This is a foundation, not a planner. It does not decompose goals into
|
|
15
|
-
* sub-tasks or maintain a world model. It trusts the LLM to drive the
|
|
16
|
-
* loop and uses the [DONE] marker or tool-call cessation as termination
|
|
17
|
-
* signals. More sophisticated planning belongs in userland, composed
|
|
18
|
-
* on top of this primitive.
|
|
19
|
-
*
|
|
20
|
-
* Key features:
|
|
21
|
-
* - Async generator yielding step results (observable, composable)
|
|
22
|
-
* - Human-in-the-loop via shouldContinue callback
|
|
23
|
-
* - Abort support via AbortController
|
|
24
|
-
* - Permission preflight before every step
|
|
25
|
-
* - Event emission on start, step, done, error
|
|
26
|
-
* - Auto-generated system prompt describing agent role and tools
|
|
27
|
-
* - Configurable step limit (default 10)
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import { logger } from '../core/wu-logger.js';
|
|
31
|
-
|
|
32
|
-
// ─── Constants ──────────────────────────────────────────────────
|
|
33
|
-
|
|
34
|
-
const DONE_MARKER = '[DONE]';
|
|
35
|
-
|
|
36
|
-
const DEFAULT_MAX_STEPS = 10;
|
|
37
|
-
|
|
38
|
-
const AGENT_NAMESPACE_PREFIX = 'agent:';
|
|
39
|
-
|
|
40
|
-
// ─── Step Result Types ──────────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @typedef {object} AgentStepResult
|
|
44
|
-
* @property {number} step - Step number (1-indexed)
|
|
45
|
-
* @property {'thinking'|'tool_call'|'done'|'blocked'|'aborted'|'interrupted'} type
|
|
46
|
-
* @property {string} [content] - LLM text response for this step
|
|
47
|
-
* @property {Array} [toolResults] - Tool execution results (if tools were called)
|
|
48
|
-
* @property {object} [usage] - Token usage for this step
|
|
49
|
-
* @property {string} [reason] - Reason for termination (done/blocked/aborted)
|
|
50
|
-
* @property {number} elapsed - Time in ms for this step
|
|
51
|
-
*/
|
|
52
|
-
|
|
53
|
-
// ─── Agent Class ────────────────────────────────────────────────
|
|
54
|
-
|
|
55
|
-
export class WuAIAgent {
|
|
56
|
-
/**
|
|
57
|
-
* @param {object} deps - Injected dependencies (same pattern as other wu-ai modules)
|
|
58
|
-
* @param {import('./wu-ai-conversation.js').WuAIConversation} deps.conversation - Conversation manager
|
|
59
|
-
* @param {import('./wu-ai-actions.js').WuAIActions} deps.actions - Action registry
|
|
60
|
-
* @param {import('./wu-ai-context.js').WuAIContext} deps.context - Context collector
|
|
61
|
-
* @param {import('./wu-ai-permissions.js').WuAIPermissions} deps.permissions - Permission system
|
|
62
|
-
* @param {object} deps.eventBus - WuEventBus instance
|
|
63
|
-
*/
|
|
64
|
-
constructor({ conversation, actions, context, permissions, eventBus }) {
|
|
65
|
-
this._conversation = conversation;
|
|
66
|
-
this._actions = actions;
|
|
67
|
-
this._context = context;
|
|
68
|
-
this._permissions = permissions;
|
|
69
|
-
this._eventBus = eventBus;
|
|
70
|
-
|
|
71
|
-
this._config = {
|
|
72
|
-
maxSteps: DEFAULT_MAX_STEPS,
|
|
73
|
-
systemPrompt: null,
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
this._activeRuns = new Map(); // runId -> AbortController
|
|
77
|
-
this._stats = {
|
|
78
|
-
totalRuns: 0,
|
|
79
|
-
totalSteps: 0,
|
|
80
|
-
completedRuns: 0,
|
|
81
|
-
abortedRuns: 0,
|
|
82
|
-
errorRuns: 0,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Post-init configuration.
|
|
88
|
-
*
|
|
89
|
-
* @param {object} config
|
|
90
|
-
* @param {number} [config.maxSteps] - Default max steps for all runs
|
|
91
|
-
* @param {string|Function} [config.systemPrompt] - Default agent system prompt
|
|
92
|
-
*/
|
|
93
|
-
configure(config) {
|
|
94
|
-
if (config.maxSteps !== undefined) this._config.maxSteps = config.maxSteps;
|
|
95
|
-
if (config.systemPrompt !== undefined) this._config.systemPrompt = config.systemPrompt;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Run the agent loop toward a goal.
|
|
100
|
-
*
|
|
101
|
-
* The generator yields after each LLM round. The caller can inspect
|
|
102
|
-
* the result, update UI, or break out of the loop to abort early.
|
|
103
|
-
*
|
|
104
|
-
* Termination conditions (checked in order):
|
|
105
|
-
* 1. AbortController signal fired
|
|
106
|
-
* 2. shouldContinue() returns false (human-in-the-loop gate)
|
|
107
|
-
* 3. Permission preflight denied
|
|
108
|
-
* 4. LLM response contains [DONE] marker
|
|
109
|
-
* 5. LLM stops requesting tool calls (after previously requesting them)
|
|
110
|
-
* 6. maxSteps reached
|
|
111
|
-
*
|
|
112
|
-
* @param {string} goal - Natural language description of what to achieve
|
|
113
|
-
* @param {object} [options]
|
|
114
|
-
* @param {number} [options.maxSteps] - Override max steps for this run
|
|
115
|
-
* @param {string} [options.provider] - Use a specific registered provider
|
|
116
|
-
* @param {string} [options.namespace] - Conversation namespace (auto-generated if omitted)
|
|
117
|
-
* @param {string|Function} [options.systemPrompt] - Override system prompt
|
|
118
|
-
* @param {Function} [options.onStep] - Callback invoked after each step: (stepResult) => void
|
|
119
|
-
* @param {Function} [options.shouldContinue] - Async gate: (stepResult) => boolean. Return false to stop.
|
|
120
|
-
* @param {AbortSignal} [options.signal] - External abort signal
|
|
121
|
-
* @param {number} [options.temperature] - LLM temperature
|
|
122
|
-
* @param {number} [options.maxTokens] - LLM max tokens per step
|
|
123
|
-
* @yields {AgentStepResult}
|
|
124
|
-
*/
|
|
125
|
-
async *run(goal, options = {}) {
|
|
126
|
-
const maxSteps = options.maxSteps ?? this._config.maxSteps;
|
|
127
|
-
const namespace = options.namespace || this._generateNamespace();
|
|
128
|
-
const runId = this._generateRunId();
|
|
129
|
-
|
|
130
|
-
// Abort controller: merge external signal with our own
|
|
131
|
-
const controller = new AbortController();
|
|
132
|
-
this._activeRuns.set(runId, controller);
|
|
133
|
-
|
|
134
|
-
if (options.signal) {
|
|
135
|
-
if (options.signal.aborted) {
|
|
136
|
-
controller.abort();
|
|
137
|
-
} else {
|
|
138
|
-
options.signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
this._stats.totalRuns++;
|
|
143
|
-
|
|
144
|
-
// Build the agent system prompt
|
|
145
|
-
const systemPrompt = await this._buildAgentSystemPrompt(goal, options);
|
|
146
|
-
|
|
147
|
-
// Emit start event
|
|
148
|
-
this._eventBus.emit('ai:agent:start', {
|
|
149
|
-
runId,
|
|
150
|
-
goal,
|
|
151
|
-
namespace,
|
|
152
|
-
maxSteps,
|
|
153
|
-
}, { appName: 'wu-ai' });
|
|
154
|
-
|
|
155
|
-
logger.wuInfo(`[wu-ai] Agent run started: "${goal.slice(0, 80)}${goal.length > 80 ? '...' : ''}" (max ${maxSteps} steps)`);
|
|
156
|
-
|
|
157
|
-
let step = 0;
|
|
158
|
-
let previousHadToolCalls = false;
|
|
159
|
-
let finalReason = 'max_steps';
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
// Inject the goal as the first user message via conversation.send
|
|
163
|
-
// The loop sends the goal on step 1, then follow-up prompts on subsequent steps.
|
|
164
|
-
|
|
165
|
-
while (step < maxSteps) {
|
|
166
|
-
step++;
|
|
167
|
-
const stepStart = Date.now();
|
|
168
|
-
|
|
169
|
-
// ── 1. Check abort ──
|
|
170
|
-
if (controller.signal.aborted) {
|
|
171
|
-
const result = this._buildStepResult(step, 'aborted', {
|
|
172
|
-
reason: 'Aborted by caller',
|
|
173
|
-
elapsed: Date.now() - stepStart,
|
|
174
|
-
});
|
|
175
|
-
this._stats.abortedRuns++;
|
|
176
|
-
finalReason = 'aborted';
|
|
177
|
-
this._emitStep(runId, result);
|
|
178
|
-
yield result;
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// ── 2. Permission preflight ──
|
|
183
|
-
const traceId = this._permissions.loopProtection.createTraceId();
|
|
184
|
-
const preflight = this._permissions.preflight({
|
|
185
|
-
namespace,
|
|
186
|
-
depth: step,
|
|
187
|
-
traceId,
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
if (!preflight.allowed) {
|
|
191
|
-
const result = this._buildStepResult(step, 'blocked', {
|
|
192
|
-
reason: preflight.reason,
|
|
193
|
-
elapsed: Date.now() - stepStart,
|
|
194
|
-
});
|
|
195
|
-
finalReason = 'blocked';
|
|
196
|
-
this._emitStep(runId, result);
|
|
197
|
-
yield result;
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ── 3. Compose the message for this step ──
|
|
202
|
-
const message = step === 1
|
|
203
|
-
? goal
|
|
204
|
-
: 'Continue working toward the goal. If you are done, include [DONE] in your response.';
|
|
205
|
-
|
|
206
|
-
// ── 4. Send to conversation (handles tool call loops internally) ──
|
|
207
|
-
let response;
|
|
208
|
-
try {
|
|
209
|
-
response = await this._conversation.send(message, {
|
|
210
|
-
namespace,
|
|
211
|
-
systemPrompt,
|
|
212
|
-
provider: options.provider,
|
|
213
|
-
temperature: options.temperature,
|
|
214
|
-
maxTokens: options.maxTokens,
|
|
215
|
-
signal: controller.signal,
|
|
216
|
-
});
|
|
217
|
-
} catch (err) {
|
|
218
|
-
if (err.name === 'AbortError' || controller.signal.aborted) {
|
|
219
|
-
const result = this._buildStepResult(step, 'aborted', {
|
|
220
|
-
reason: 'Aborted during LLM call',
|
|
221
|
-
elapsed: Date.now() - stepStart,
|
|
222
|
-
});
|
|
223
|
-
this._stats.abortedRuns++;
|
|
224
|
-
finalReason = 'aborted';
|
|
225
|
-
this._emitStep(runId, result);
|
|
226
|
-
yield result;
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
throw err;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const elapsed = Date.now() - stepStart;
|
|
233
|
-
this._stats.totalSteps++;
|
|
234
|
-
|
|
235
|
-
const hasToolResults = response.tool_results && response.tool_results.length > 0;
|
|
236
|
-
const content = response.content || '';
|
|
237
|
-
|
|
238
|
-
// ── 5. Check for DONE marker ──
|
|
239
|
-
if (content.includes(DONE_MARKER)) {
|
|
240
|
-
const result = this._buildStepResult(step, 'done', {
|
|
241
|
-
content: content.replace(DONE_MARKER, '').trim(),
|
|
242
|
-
toolResults: response.tool_results,
|
|
243
|
-
usage: response.usage,
|
|
244
|
-
reason: 'Goal completed (DONE marker)',
|
|
245
|
-
elapsed,
|
|
246
|
-
});
|
|
247
|
-
finalReason = 'done';
|
|
248
|
-
this._stats.completedRuns++;
|
|
249
|
-
this._emitStep(runId, result);
|
|
250
|
-
if (options.onStep) await this._safeCallback(options.onStep, result);
|
|
251
|
-
yield result;
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// ── 6. Check for tool-call cessation ──
|
|
256
|
-
// If the LLM was previously calling tools but stopped, it has
|
|
257
|
-
// settled on a final answer. This is the implicit completion signal.
|
|
258
|
-
const currentHasToolCalls = hasToolResults;
|
|
259
|
-
if (previousHadToolCalls && !currentHasToolCalls) {
|
|
260
|
-
const result = this._buildStepResult(step, 'done', {
|
|
261
|
-
content,
|
|
262
|
-
toolResults: response.tool_results,
|
|
263
|
-
usage: response.usage,
|
|
264
|
-
reason: 'Goal completed (no further tool calls)',
|
|
265
|
-
elapsed,
|
|
266
|
-
});
|
|
267
|
-
finalReason = 'done';
|
|
268
|
-
this._stats.completedRuns++;
|
|
269
|
-
this._emitStep(runId, result);
|
|
270
|
-
if (options.onStep) await this._safeCallback(options.onStep, result);
|
|
271
|
-
yield result;
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
previousHadToolCalls = currentHasToolCalls;
|
|
276
|
-
|
|
277
|
-
// ── 7. Yield the step result ──
|
|
278
|
-
const stepType = hasToolResults ? 'tool_call' : 'thinking';
|
|
279
|
-
const result = this._buildStepResult(step, stepType, {
|
|
280
|
-
content,
|
|
281
|
-
toolResults: response.tool_results,
|
|
282
|
-
usage: response.usage,
|
|
283
|
-
elapsed,
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
this._emitStep(runId, result);
|
|
287
|
-
if (options.onStep) await this._safeCallback(options.onStep, result);
|
|
288
|
-
yield result;
|
|
289
|
-
|
|
290
|
-
// ── 8. Human-in-the-loop gate ──
|
|
291
|
-
if (options.shouldContinue) {
|
|
292
|
-
let shouldGo;
|
|
293
|
-
try {
|
|
294
|
-
shouldGo = await options.shouldContinue(result);
|
|
295
|
-
} catch {
|
|
296
|
-
shouldGo = false;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (!shouldGo) {
|
|
300
|
-
const interrupted = this._buildStepResult(step, 'interrupted', {
|
|
301
|
-
content,
|
|
302
|
-
reason: 'Stopped by shouldContinue callback',
|
|
303
|
-
elapsed: 0,
|
|
304
|
-
});
|
|
305
|
-
finalReason = 'interrupted';
|
|
306
|
-
this._emitStep(runId, interrupted);
|
|
307
|
-
yield interrupted;
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// ── Max steps reached ──
|
|
314
|
-
const maxStepResult = this._buildStepResult(step, 'done', {
|
|
315
|
-
reason: `Max steps (${maxSteps}) reached`,
|
|
316
|
-
elapsed: 0,
|
|
317
|
-
});
|
|
318
|
-
finalReason = 'max_steps';
|
|
319
|
-
yield maxStepResult;
|
|
320
|
-
|
|
321
|
-
} catch (err) {
|
|
322
|
-
this._stats.errorRuns++;
|
|
323
|
-
finalReason = 'error';
|
|
324
|
-
|
|
325
|
-
logger.wuWarn(`[wu-ai] Agent run error: ${err.message}`);
|
|
326
|
-
|
|
327
|
-
this._eventBus.emit('ai:agent:error', {
|
|
328
|
-
runId,
|
|
329
|
-
goal,
|
|
330
|
-
namespace,
|
|
331
|
-
step,
|
|
332
|
-
error: err.message,
|
|
333
|
-
}, { appName: 'wu-ai' });
|
|
334
|
-
|
|
335
|
-
throw err;
|
|
336
|
-
} finally {
|
|
337
|
-
// Clean up
|
|
338
|
-
this._activeRuns.delete(runId);
|
|
339
|
-
|
|
340
|
-
// Delete auto-generated agent namespaces to prevent memory leaks
|
|
341
|
-
// User-provided namespaces are preserved (the user owns their lifecycle)
|
|
342
|
-
if (!options.namespace && namespace.startsWith(AGENT_NAMESPACE_PREFIX)) {
|
|
343
|
-
this._conversation.deleteNamespace(namespace);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
this._eventBus.emit('ai:agent:done', {
|
|
347
|
-
runId,
|
|
348
|
-
goal,
|
|
349
|
-
namespace,
|
|
350
|
-
totalSteps: step,
|
|
351
|
-
reason: finalReason,
|
|
352
|
-
}, { appName: 'wu-ai' });
|
|
353
|
-
|
|
354
|
-
logger.wuDebug(`[wu-ai] Agent run finished: ${finalReason} after ${step} step(s)`);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Abort an active agent run by runId.
|
|
360
|
-
*
|
|
361
|
-
* @param {string} runId - The run ID to abort
|
|
362
|
-
*/
|
|
363
|
-
abort(runId) {
|
|
364
|
-
const controller = this._activeRuns.get(runId);
|
|
365
|
-
if (controller) {
|
|
366
|
-
controller.abort();
|
|
367
|
-
logger.wuDebug(`[wu-ai] Agent run aborted: ${runId}`);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Abort all active agent runs.
|
|
373
|
-
*/
|
|
374
|
-
abortAll() {
|
|
375
|
-
for (const [runId, controller] of this._activeRuns) {
|
|
376
|
-
controller.abort();
|
|
377
|
-
}
|
|
378
|
-
this._activeRuns.clear();
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* Get IDs of currently active runs.
|
|
383
|
-
*
|
|
384
|
-
* @returns {string[]}
|
|
385
|
-
*/
|
|
386
|
-
getActiveRuns() {
|
|
387
|
-
return [...this._activeRuns.keys()];
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Get agent statistics.
|
|
392
|
-
*
|
|
393
|
-
* @returns {object}
|
|
394
|
-
*/
|
|
395
|
-
getStats() {
|
|
396
|
-
return {
|
|
397
|
-
...this._stats,
|
|
398
|
-
activeRuns: this._activeRuns.size,
|
|
399
|
-
config: { ...this._config },
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Destroy the agent, aborting all active runs.
|
|
405
|
-
*/
|
|
406
|
-
destroy() {
|
|
407
|
-
this.abortAll();
|
|
408
|
-
this._stats = {
|
|
409
|
-
totalRuns: 0,
|
|
410
|
-
totalSteps: 0,
|
|
411
|
-
completedRuns: 0,
|
|
412
|
-
abortedRuns: 0,
|
|
413
|
-
errorRuns: 0,
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// ─── Private ──────────────────────────────────────────────────
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Build the agent-specific system prompt. This tells the LLM it is
|
|
421
|
-
* operating as an autonomous agent with a goal, available tools, and
|
|
422
|
-
* the [DONE] completion protocol.
|
|
423
|
-
*/
|
|
424
|
-
async _buildAgentSystemPrompt(goal, options) {
|
|
425
|
-
// Explicit override takes precedence
|
|
426
|
-
const basePrompt = options.systemPrompt
|
|
427
|
-
?? this._config.systemPrompt
|
|
428
|
-
?? null;
|
|
429
|
-
|
|
430
|
-
if (basePrompt) {
|
|
431
|
-
const resolved = typeof basePrompt === 'function'
|
|
432
|
-
? await basePrompt(goal)
|
|
433
|
-
: basePrompt;
|
|
434
|
-
return resolved;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Auto-generate from context and tools
|
|
438
|
-
const parts = [];
|
|
439
|
-
|
|
440
|
-
parts.push(
|
|
441
|
-
'You are an autonomous AI agent connected to a live web application via Wu Framework.',
|
|
442
|
-
'You have been given a goal and must work step-by-step to achieve it.',
|
|
443
|
-
'',
|
|
444
|
-
'PROTOCOL:',
|
|
445
|
-
'- Each message you send is one "step" in your execution.',
|
|
446
|
-
'- You may call tools to read or modify application state.',
|
|
447
|
-
'- After each step, you will be prompted to continue.',
|
|
448
|
-
'- When the goal is fully achieved, include the marker [DONE] in your response.',
|
|
449
|
-
'- If you determine the goal cannot be achieved, include [DONE] and explain why.',
|
|
450
|
-
'- Be concise. Each step should make meaningful progress.',
|
|
451
|
-
'',
|
|
452
|
-
);
|
|
453
|
-
|
|
454
|
-
// Collect context if available
|
|
455
|
-
if (this._context) {
|
|
456
|
-
try {
|
|
457
|
-
await this._context.collect();
|
|
458
|
-
const snapshot = this._context.getSnapshot();
|
|
459
|
-
if (snapshot?._mountedApps?.length) {
|
|
460
|
-
parts.push(`MOUNTED APPS: ${snapshot._mountedApps.join(', ')}`, '');
|
|
461
|
-
}
|
|
462
|
-
if (snapshot?._store && Object.keys(snapshot._store).length > 0) {
|
|
463
|
-
parts.push(`APPLICATION STATE:\n${JSON.stringify(snapshot._store, null, 2)}`, '');
|
|
464
|
-
}
|
|
465
|
-
} catch {
|
|
466
|
-
// Context collection is best-effort
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// List available tools
|
|
471
|
-
const tools = this._actions.getToolSchemas();
|
|
472
|
-
if (tools.length > 0) {
|
|
473
|
-
parts.push('AVAILABLE TOOLS:');
|
|
474
|
-
for (const tool of tools) {
|
|
475
|
-
const paramKeys = tool.parameters?.properties
|
|
476
|
-
? Object.keys(tool.parameters.properties).join(', ')
|
|
477
|
-
: 'none';
|
|
478
|
-
parts.push(`- ${tool.name}(${paramKeys}): ${tool.description}`);
|
|
479
|
-
}
|
|
480
|
-
parts.push('');
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
parts.push(`GOAL: ${goal}`);
|
|
484
|
-
|
|
485
|
-
return parts.join('\n');
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* Build a normalized step result object.
|
|
490
|
-
*
|
|
491
|
-
* @param {number} step
|
|
492
|
-
* @param {string} type
|
|
493
|
-
* @param {object} data
|
|
494
|
-
* @returns {AgentStepResult}
|
|
495
|
-
*/
|
|
496
|
-
_buildStepResult(step, type, data = {}) {
|
|
497
|
-
return {
|
|
498
|
-
step,
|
|
499
|
-
type,
|
|
500
|
-
content: data.content ?? null,
|
|
501
|
-
toolResults: data.toolResults ?? null,
|
|
502
|
-
usage: data.usage ?? null,
|
|
503
|
-
reason: data.reason ?? null,
|
|
504
|
-
elapsed: data.elapsed ?? 0,
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* Emit an ai:agent:step event.
|
|
510
|
-
*/
|
|
511
|
-
_emitStep(runId, result) {
|
|
512
|
-
this._eventBus.emit('ai:agent:step', {
|
|
513
|
-
runId,
|
|
514
|
-
...result,
|
|
515
|
-
}, { appName: 'wu-ai' });
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Safely invoke a callback, swallowing errors so the agent loop
|
|
520
|
-
* is never broken by a faulty onStep handler.
|
|
521
|
-
*/
|
|
522
|
-
async _safeCallback(fn, ...args) {
|
|
523
|
-
try {
|
|
524
|
-
const result = fn(...args);
|
|
525
|
-
if (result && typeof result.then === 'function') {
|
|
526
|
-
await result;
|
|
527
|
-
}
|
|
528
|
-
} catch (err) {
|
|
529
|
-
logger.wuDebug(`[wu-ai] Agent callback error: ${err.message}`);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/**
|
|
534
|
-
* Generate a unique namespace for an agent run.
|
|
535
|
-
*/
|
|
536
|
-
_generateNamespace() {
|
|
537
|
-
return AGENT_NAMESPACE_PREFIX + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 6);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
/**
|
|
541
|
-
* Generate a unique run ID.
|
|
542
|
-
*/
|
|
543
|
-
_generateRunId() {
|
|
544
|
-
return 'run_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8);
|
|
545
|
-
}
|
|
546
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* WU-AI-AGENT: Autonomous agent loop (Paradigm 3)
|
|
3
|
+
*
|
|
4
|
+
* The agent is the third 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 (this file)
|
|
8
|
+
*
|
|
9
|
+
* The agent receives a goal and iterates: send to LLM, execute tool calls,
|
|
10
|
+
* observe results, repeat — until the goal is achieved or limits are hit.
|
|
11
|
+
* It is an async generator, yielding each step so the caller can observe,
|
|
12
|
+
* log, render, or intervene at any point.
|
|
13
|
+
*
|
|
14
|
+
* This is a foundation, not a planner. It does not decompose goals into
|
|
15
|
+
* sub-tasks or maintain a world model. It trusts the LLM to drive the
|
|
16
|
+
* loop and uses the [DONE] marker or tool-call cessation as termination
|
|
17
|
+
* signals. More sophisticated planning belongs in userland, composed
|
|
18
|
+
* on top of this primitive.
|
|
19
|
+
*
|
|
20
|
+
* Key features:
|
|
21
|
+
* - Async generator yielding step results (observable, composable)
|
|
22
|
+
* - Human-in-the-loop via shouldContinue callback
|
|
23
|
+
* - Abort support via AbortController
|
|
24
|
+
* - Permission preflight before every step
|
|
25
|
+
* - Event emission on start, step, done, error
|
|
26
|
+
* - Auto-generated system prompt describing agent role and tools
|
|
27
|
+
* - Configurable step limit (default 10)
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { logger } from '../core/wu-logger.js';
|
|
31
|
+
|
|
32
|
+
// ─── Constants ──────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
const DONE_MARKER = '[DONE]';
|
|
35
|
+
|
|
36
|
+
const DEFAULT_MAX_STEPS = 10;
|
|
37
|
+
|
|
38
|
+
const AGENT_NAMESPACE_PREFIX = 'agent:';
|
|
39
|
+
|
|
40
|
+
// ─── Step Result Types ──────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {object} AgentStepResult
|
|
44
|
+
* @property {number} step - Step number (1-indexed)
|
|
45
|
+
* @property {'thinking'|'tool_call'|'done'|'blocked'|'aborted'|'interrupted'} type
|
|
46
|
+
* @property {string} [content] - LLM text response for this step
|
|
47
|
+
* @property {Array} [toolResults] - Tool execution results (if tools were called)
|
|
48
|
+
* @property {object} [usage] - Token usage for this step
|
|
49
|
+
* @property {string} [reason] - Reason for termination (done/blocked/aborted)
|
|
50
|
+
* @property {number} elapsed - Time in ms for this step
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
// ─── Agent Class ────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
export class WuAIAgent {
|
|
56
|
+
/**
|
|
57
|
+
* @param {object} deps - Injected dependencies (same pattern as other wu-ai modules)
|
|
58
|
+
* @param {import('./wu-ai-conversation.js').WuAIConversation} deps.conversation - Conversation manager
|
|
59
|
+
* @param {import('./wu-ai-actions.js').WuAIActions} deps.actions - Action registry
|
|
60
|
+
* @param {import('./wu-ai-context.js').WuAIContext} deps.context - Context collector
|
|
61
|
+
* @param {import('./wu-ai-permissions.js').WuAIPermissions} deps.permissions - Permission system
|
|
62
|
+
* @param {object} deps.eventBus - WuEventBus instance
|
|
63
|
+
*/
|
|
64
|
+
constructor({ conversation, actions, context, permissions, eventBus }) {
|
|
65
|
+
this._conversation = conversation;
|
|
66
|
+
this._actions = actions;
|
|
67
|
+
this._context = context;
|
|
68
|
+
this._permissions = permissions;
|
|
69
|
+
this._eventBus = eventBus;
|
|
70
|
+
|
|
71
|
+
this._config = {
|
|
72
|
+
maxSteps: DEFAULT_MAX_STEPS,
|
|
73
|
+
systemPrompt: null,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
this._activeRuns = new Map(); // runId -> AbortController
|
|
77
|
+
this._stats = {
|
|
78
|
+
totalRuns: 0,
|
|
79
|
+
totalSteps: 0,
|
|
80
|
+
completedRuns: 0,
|
|
81
|
+
abortedRuns: 0,
|
|
82
|
+
errorRuns: 0,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Post-init configuration.
|
|
88
|
+
*
|
|
89
|
+
* @param {object} config
|
|
90
|
+
* @param {number} [config.maxSteps] - Default max steps for all runs
|
|
91
|
+
* @param {string|Function} [config.systemPrompt] - Default agent system prompt
|
|
92
|
+
*/
|
|
93
|
+
configure(config) {
|
|
94
|
+
if (config.maxSteps !== undefined) this._config.maxSteps = config.maxSteps;
|
|
95
|
+
if (config.systemPrompt !== undefined) this._config.systemPrompt = config.systemPrompt;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Run the agent loop toward a goal.
|
|
100
|
+
*
|
|
101
|
+
* The generator yields after each LLM round. The caller can inspect
|
|
102
|
+
* the result, update UI, or break out of the loop to abort early.
|
|
103
|
+
*
|
|
104
|
+
* Termination conditions (checked in order):
|
|
105
|
+
* 1. AbortController signal fired
|
|
106
|
+
* 2. shouldContinue() returns false (human-in-the-loop gate)
|
|
107
|
+
* 3. Permission preflight denied
|
|
108
|
+
* 4. LLM response contains [DONE] marker
|
|
109
|
+
* 5. LLM stops requesting tool calls (after previously requesting them)
|
|
110
|
+
* 6. maxSteps reached
|
|
111
|
+
*
|
|
112
|
+
* @param {string} goal - Natural language description of what to achieve
|
|
113
|
+
* @param {object} [options]
|
|
114
|
+
* @param {number} [options.maxSteps] - Override max steps for this run
|
|
115
|
+
* @param {string} [options.provider] - Use a specific registered provider
|
|
116
|
+
* @param {string} [options.namespace] - Conversation namespace (auto-generated if omitted)
|
|
117
|
+
* @param {string|Function} [options.systemPrompt] - Override system prompt
|
|
118
|
+
* @param {Function} [options.onStep] - Callback invoked after each step: (stepResult) => void
|
|
119
|
+
* @param {Function} [options.shouldContinue] - Async gate: (stepResult) => boolean. Return false to stop.
|
|
120
|
+
* @param {AbortSignal} [options.signal] - External abort signal
|
|
121
|
+
* @param {number} [options.temperature] - LLM temperature
|
|
122
|
+
* @param {number} [options.maxTokens] - LLM max tokens per step
|
|
123
|
+
* @yields {AgentStepResult}
|
|
124
|
+
*/
|
|
125
|
+
async *run(goal, options = {}) {
|
|
126
|
+
const maxSteps = options.maxSteps ?? this._config.maxSteps;
|
|
127
|
+
const namespace = options.namespace || this._generateNamespace();
|
|
128
|
+
const runId = this._generateRunId();
|
|
129
|
+
|
|
130
|
+
// Abort controller: merge external signal with our own
|
|
131
|
+
const controller = new AbortController();
|
|
132
|
+
this._activeRuns.set(runId, controller);
|
|
133
|
+
|
|
134
|
+
if (options.signal) {
|
|
135
|
+
if (options.signal.aborted) {
|
|
136
|
+
controller.abort();
|
|
137
|
+
} else {
|
|
138
|
+
options.signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this._stats.totalRuns++;
|
|
143
|
+
|
|
144
|
+
// Build the agent system prompt
|
|
145
|
+
const systemPrompt = await this._buildAgentSystemPrompt(goal, options);
|
|
146
|
+
|
|
147
|
+
// Emit start event
|
|
148
|
+
this._eventBus.emit('ai:agent:start', {
|
|
149
|
+
runId,
|
|
150
|
+
goal,
|
|
151
|
+
namespace,
|
|
152
|
+
maxSteps,
|
|
153
|
+
}, { appName: 'wu-ai' });
|
|
154
|
+
|
|
155
|
+
logger.wuInfo(`[wu-ai] Agent run started: "${goal.slice(0, 80)}${goal.length > 80 ? '...' : ''}" (max ${maxSteps} steps)`);
|
|
156
|
+
|
|
157
|
+
let step = 0;
|
|
158
|
+
let previousHadToolCalls = false;
|
|
159
|
+
let finalReason = 'max_steps';
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
// Inject the goal as the first user message via conversation.send
|
|
163
|
+
// The loop sends the goal on step 1, then follow-up prompts on subsequent steps.
|
|
164
|
+
|
|
165
|
+
while (step < maxSteps) {
|
|
166
|
+
step++;
|
|
167
|
+
const stepStart = Date.now();
|
|
168
|
+
|
|
169
|
+
// ── 1. Check abort ──
|
|
170
|
+
if (controller.signal.aborted) {
|
|
171
|
+
const result = this._buildStepResult(step, 'aborted', {
|
|
172
|
+
reason: 'Aborted by caller',
|
|
173
|
+
elapsed: Date.now() - stepStart,
|
|
174
|
+
});
|
|
175
|
+
this._stats.abortedRuns++;
|
|
176
|
+
finalReason = 'aborted';
|
|
177
|
+
this._emitStep(runId, result);
|
|
178
|
+
yield result;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── 2. Permission preflight ──
|
|
183
|
+
const traceId = this._permissions.loopProtection.createTraceId();
|
|
184
|
+
const preflight = this._permissions.preflight({
|
|
185
|
+
namespace,
|
|
186
|
+
depth: step,
|
|
187
|
+
traceId,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (!preflight.allowed) {
|
|
191
|
+
const result = this._buildStepResult(step, 'blocked', {
|
|
192
|
+
reason: preflight.reason,
|
|
193
|
+
elapsed: Date.now() - stepStart,
|
|
194
|
+
});
|
|
195
|
+
finalReason = 'blocked';
|
|
196
|
+
this._emitStep(runId, result);
|
|
197
|
+
yield result;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── 3. Compose the message for this step ──
|
|
202
|
+
const message = step === 1
|
|
203
|
+
? goal
|
|
204
|
+
: 'Continue working toward the goal. If you are done, include [DONE] in your response.';
|
|
205
|
+
|
|
206
|
+
// ── 4. Send to conversation (handles tool call loops internally) ──
|
|
207
|
+
let response;
|
|
208
|
+
try {
|
|
209
|
+
response = await this._conversation.send(message, {
|
|
210
|
+
namespace,
|
|
211
|
+
systemPrompt,
|
|
212
|
+
provider: options.provider,
|
|
213
|
+
temperature: options.temperature,
|
|
214
|
+
maxTokens: options.maxTokens,
|
|
215
|
+
signal: controller.signal,
|
|
216
|
+
});
|
|
217
|
+
} catch (err) {
|
|
218
|
+
if (err.name === 'AbortError' || controller.signal.aborted) {
|
|
219
|
+
const result = this._buildStepResult(step, 'aborted', {
|
|
220
|
+
reason: 'Aborted during LLM call',
|
|
221
|
+
elapsed: Date.now() - stepStart,
|
|
222
|
+
});
|
|
223
|
+
this._stats.abortedRuns++;
|
|
224
|
+
finalReason = 'aborted';
|
|
225
|
+
this._emitStep(runId, result);
|
|
226
|
+
yield result;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
throw err;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const elapsed = Date.now() - stepStart;
|
|
233
|
+
this._stats.totalSteps++;
|
|
234
|
+
|
|
235
|
+
const hasToolResults = response.tool_results && response.tool_results.length > 0;
|
|
236
|
+
const content = response.content || '';
|
|
237
|
+
|
|
238
|
+
// ── 5. Check for DONE marker ──
|
|
239
|
+
if (content.includes(DONE_MARKER)) {
|
|
240
|
+
const result = this._buildStepResult(step, 'done', {
|
|
241
|
+
content: content.replace(DONE_MARKER, '').trim(),
|
|
242
|
+
toolResults: response.tool_results,
|
|
243
|
+
usage: response.usage,
|
|
244
|
+
reason: 'Goal completed (DONE marker)',
|
|
245
|
+
elapsed,
|
|
246
|
+
});
|
|
247
|
+
finalReason = 'done';
|
|
248
|
+
this._stats.completedRuns++;
|
|
249
|
+
this._emitStep(runId, result);
|
|
250
|
+
if (options.onStep) await this._safeCallback(options.onStep, result);
|
|
251
|
+
yield result;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── 6. Check for tool-call cessation ──
|
|
256
|
+
// If the LLM was previously calling tools but stopped, it has
|
|
257
|
+
// settled on a final answer. This is the implicit completion signal.
|
|
258
|
+
const currentHasToolCalls = hasToolResults;
|
|
259
|
+
if (previousHadToolCalls && !currentHasToolCalls) {
|
|
260
|
+
const result = this._buildStepResult(step, 'done', {
|
|
261
|
+
content,
|
|
262
|
+
toolResults: response.tool_results,
|
|
263
|
+
usage: response.usage,
|
|
264
|
+
reason: 'Goal completed (no further tool calls)',
|
|
265
|
+
elapsed,
|
|
266
|
+
});
|
|
267
|
+
finalReason = 'done';
|
|
268
|
+
this._stats.completedRuns++;
|
|
269
|
+
this._emitStep(runId, result);
|
|
270
|
+
if (options.onStep) await this._safeCallback(options.onStep, result);
|
|
271
|
+
yield result;
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
previousHadToolCalls = currentHasToolCalls;
|
|
276
|
+
|
|
277
|
+
// ── 7. Yield the step result ──
|
|
278
|
+
const stepType = hasToolResults ? 'tool_call' : 'thinking';
|
|
279
|
+
const result = this._buildStepResult(step, stepType, {
|
|
280
|
+
content,
|
|
281
|
+
toolResults: response.tool_results,
|
|
282
|
+
usage: response.usage,
|
|
283
|
+
elapsed,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
this._emitStep(runId, result);
|
|
287
|
+
if (options.onStep) await this._safeCallback(options.onStep, result);
|
|
288
|
+
yield result;
|
|
289
|
+
|
|
290
|
+
// ── 8. Human-in-the-loop gate ──
|
|
291
|
+
if (options.shouldContinue) {
|
|
292
|
+
let shouldGo;
|
|
293
|
+
try {
|
|
294
|
+
shouldGo = await options.shouldContinue(result);
|
|
295
|
+
} catch {
|
|
296
|
+
shouldGo = false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!shouldGo) {
|
|
300
|
+
const interrupted = this._buildStepResult(step, 'interrupted', {
|
|
301
|
+
content,
|
|
302
|
+
reason: 'Stopped by shouldContinue callback',
|
|
303
|
+
elapsed: 0,
|
|
304
|
+
});
|
|
305
|
+
finalReason = 'interrupted';
|
|
306
|
+
this._emitStep(runId, interrupted);
|
|
307
|
+
yield interrupted;
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ── Max steps reached ──
|
|
314
|
+
const maxStepResult = this._buildStepResult(step, 'done', {
|
|
315
|
+
reason: `Max steps (${maxSteps}) reached`,
|
|
316
|
+
elapsed: 0,
|
|
317
|
+
});
|
|
318
|
+
finalReason = 'max_steps';
|
|
319
|
+
yield maxStepResult;
|
|
320
|
+
|
|
321
|
+
} catch (err) {
|
|
322
|
+
this._stats.errorRuns++;
|
|
323
|
+
finalReason = 'error';
|
|
324
|
+
|
|
325
|
+
logger.wuWarn(`[wu-ai] Agent run error: ${err.message}`);
|
|
326
|
+
|
|
327
|
+
this._eventBus.emit('ai:agent:error', {
|
|
328
|
+
runId,
|
|
329
|
+
goal,
|
|
330
|
+
namespace,
|
|
331
|
+
step,
|
|
332
|
+
error: err.message,
|
|
333
|
+
}, { appName: 'wu-ai' });
|
|
334
|
+
|
|
335
|
+
throw err;
|
|
336
|
+
} finally {
|
|
337
|
+
// Clean up
|
|
338
|
+
this._activeRuns.delete(runId);
|
|
339
|
+
|
|
340
|
+
// Delete auto-generated agent namespaces to prevent memory leaks
|
|
341
|
+
// User-provided namespaces are preserved (the user owns their lifecycle)
|
|
342
|
+
if (!options.namespace && namespace.startsWith(AGENT_NAMESPACE_PREFIX)) {
|
|
343
|
+
this._conversation.deleteNamespace(namespace);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
this._eventBus.emit('ai:agent:done', {
|
|
347
|
+
runId,
|
|
348
|
+
goal,
|
|
349
|
+
namespace,
|
|
350
|
+
totalSteps: step,
|
|
351
|
+
reason: finalReason,
|
|
352
|
+
}, { appName: 'wu-ai' });
|
|
353
|
+
|
|
354
|
+
logger.wuDebug(`[wu-ai] Agent run finished: ${finalReason} after ${step} step(s)`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Abort an active agent run by runId.
|
|
360
|
+
*
|
|
361
|
+
* @param {string} runId - The run ID to abort
|
|
362
|
+
*/
|
|
363
|
+
abort(runId) {
|
|
364
|
+
const controller = this._activeRuns.get(runId);
|
|
365
|
+
if (controller) {
|
|
366
|
+
controller.abort();
|
|
367
|
+
logger.wuDebug(`[wu-ai] Agent run aborted: ${runId}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Abort all active agent runs.
|
|
373
|
+
*/
|
|
374
|
+
abortAll() {
|
|
375
|
+
for (const [runId, controller] of this._activeRuns) {
|
|
376
|
+
controller.abort();
|
|
377
|
+
}
|
|
378
|
+
this._activeRuns.clear();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Get IDs of currently active runs.
|
|
383
|
+
*
|
|
384
|
+
* @returns {string[]}
|
|
385
|
+
*/
|
|
386
|
+
getActiveRuns() {
|
|
387
|
+
return [...this._activeRuns.keys()];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Get agent statistics.
|
|
392
|
+
*
|
|
393
|
+
* @returns {object}
|
|
394
|
+
*/
|
|
395
|
+
getStats() {
|
|
396
|
+
return {
|
|
397
|
+
...this._stats,
|
|
398
|
+
activeRuns: this._activeRuns.size,
|
|
399
|
+
config: { ...this._config },
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Destroy the agent, aborting all active runs.
|
|
405
|
+
*/
|
|
406
|
+
destroy() {
|
|
407
|
+
this.abortAll();
|
|
408
|
+
this._stats = {
|
|
409
|
+
totalRuns: 0,
|
|
410
|
+
totalSteps: 0,
|
|
411
|
+
completedRuns: 0,
|
|
412
|
+
abortedRuns: 0,
|
|
413
|
+
errorRuns: 0,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ─── Private ──────────────────────────────────────────────────
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Build the agent-specific system prompt. This tells the LLM it is
|
|
421
|
+
* operating as an autonomous agent with a goal, available tools, and
|
|
422
|
+
* the [DONE] completion protocol.
|
|
423
|
+
*/
|
|
424
|
+
async _buildAgentSystemPrompt(goal, options) {
|
|
425
|
+
// Explicit override takes precedence
|
|
426
|
+
const basePrompt = options.systemPrompt
|
|
427
|
+
?? this._config.systemPrompt
|
|
428
|
+
?? null;
|
|
429
|
+
|
|
430
|
+
if (basePrompt) {
|
|
431
|
+
const resolved = typeof basePrompt === 'function'
|
|
432
|
+
? await basePrompt(goal)
|
|
433
|
+
: basePrompt;
|
|
434
|
+
return resolved;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Auto-generate from context and tools
|
|
438
|
+
const parts = [];
|
|
439
|
+
|
|
440
|
+
parts.push(
|
|
441
|
+
'You are an autonomous AI agent connected to a live web application via Wu Framework.',
|
|
442
|
+
'You have been given a goal and must work step-by-step to achieve it.',
|
|
443
|
+
'',
|
|
444
|
+
'PROTOCOL:',
|
|
445
|
+
'- Each message you send is one "step" in your execution.',
|
|
446
|
+
'- You may call tools to read or modify application state.',
|
|
447
|
+
'- After each step, you will be prompted to continue.',
|
|
448
|
+
'- When the goal is fully achieved, include the marker [DONE] in your response.',
|
|
449
|
+
'- If you determine the goal cannot be achieved, include [DONE] and explain why.',
|
|
450
|
+
'- Be concise. Each step should make meaningful progress.',
|
|
451
|
+
'',
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
// Collect context if available
|
|
455
|
+
if (this._context) {
|
|
456
|
+
try {
|
|
457
|
+
await this._context.collect();
|
|
458
|
+
const snapshot = this._context.getSnapshot();
|
|
459
|
+
if (snapshot?._mountedApps?.length) {
|
|
460
|
+
parts.push(`MOUNTED APPS: ${snapshot._mountedApps.join(', ')}`, '');
|
|
461
|
+
}
|
|
462
|
+
if (snapshot?._store && Object.keys(snapshot._store).length > 0) {
|
|
463
|
+
parts.push(`APPLICATION STATE:\n${JSON.stringify(snapshot._store, null, 2)}`, '');
|
|
464
|
+
}
|
|
465
|
+
} catch {
|
|
466
|
+
// Context collection is best-effort
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// List available tools
|
|
471
|
+
const tools = this._actions.getToolSchemas();
|
|
472
|
+
if (tools.length > 0) {
|
|
473
|
+
parts.push('AVAILABLE TOOLS:');
|
|
474
|
+
for (const tool of tools) {
|
|
475
|
+
const paramKeys = tool.parameters?.properties
|
|
476
|
+
? Object.keys(tool.parameters.properties).join(', ')
|
|
477
|
+
: 'none';
|
|
478
|
+
parts.push(`- ${tool.name}(${paramKeys}): ${tool.description}`);
|
|
479
|
+
}
|
|
480
|
+
parts.push('');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
parts.push(`GOAL: ${goal}`);
|
|
484
|
+
|
|
485
|
+
return parts.join('\n');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Build a normalized step result object.
|
|
490
|
+
*
|
|
491
|
+
* @param {number} step
|
|
492
|
+
* @param {string} type
|
|
493
|
+
* @param {object} data
|
|
494
|
+
* @returns {AgentStepResult}
|
|
495
|
+
*/
|
|
496
|
+
_buildStepResult(step, type, data = {}) {
|
|
497
|
+
return {
|
|
498
|
+
step,
|
|
499
|
+
type,
|
|
500
|
+
content: data.content ?? null,
|
|
501
|
+
toolResults: data.toolResults ?? null,
|
|
502
|
+
usage: data.usage ?? null,
|
|
503
|
+
reason: data.reason ?? null,
|
|
504
|
+
elapsed: data.elapsed ?? 0,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Emit an ai:agent:step event.
|
|
510
|
+
*/
|
|
511
|
+
_emitStep(runId, result) {
|
|
512
|
+
this._eventBus.emit('ai:agent:step', {
|
|
513
|
+
runId,
|
|
514
|
+
...result,
|
|
515
|
+
}, { appName: 'wu-ai' });
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Safely invoke a callback, swallowing errors so the agent loop
|
|
520
|
+
* is never broken by a faulty onStep handler.
|
|
521
|
+
*/
|
|
522
|
+
async _safeCallback(fn, ...args) {
|
|
523
|
+
try {
|
|
524
|
+
const result = fn(...args);
|
|
525
|
+
if (result && typeof result.then === 'function') {
|
|
526
|
+
await result;
|
|
527
|
+
}
|
|
528
|
+
} catch (err) {
|
|
529
|
+
logger.wuDebug(`[wu-ai] Agent callback error: ${err.message}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Generate a unique namespace for an agent run.
|
|
535
|
+
*/
|
|
536
|
+
_generateNamespace() {
|
|
537
|
+
return AGENT_NAMESPACE_PREFIX + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 6);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Generate a unique run ID.
|
|
542
|
+
*/
|
|
543
|
+
_generateRunId() {
|
|
544
|
+
return 'run_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8);
|
|
545
|
+
}
|
|
546
|
+
}
|