musubi-sdd 3.10.0 → 5.0.0
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 +24 -19
- package/package.json +1 -1
- package/src/agents/agent-loop.js +532 -0
- package/src/agents/agentic/code-generator.js +767 -0
- package/src/agents/agentic/code-reviewer.js +698 -0
- package/src/agents/agentic/index.js +43 -0
- package/src/agents/function-tool.js +432 -0
- package/src/agents/index.js +45 -0
- package/src/agents/schema-generator.js +514 -0
- package/src/analyzers/ast-extractor.js +863 -0
- package/src/analyzers/context-optimizer.js +674 -0
- package/src/analyzers/repository-map.js +685 -0
- package/src/integrations/index.js +7 -1
- package/src/integrations/mcp/index.js +175 -0
- package/src/integrations/mcp/mcp-context-provider.js +472 -0
- package/src/integrations/mcp/mcp-discovery.js +436 -0
- package/src/integrations/mcp/mcp-tool-registry.js +467 -0
- package/src/integrations/mcp-connector.js +818 -0
- package/src/integrations/tool-discovery.js +589 -0
- package/src/managers/index.js +7 -0
- package/src/managers/skill-tools.js +565 -0
- package/src/monitoring/quality-dashboard.js +483 -0
- package/src/orchestration/agent-skill-binding.js +655 -0
- package/src/orchestration/error-handler.js +827 -0
- package/src/orchestration/index.js +235 -1
- package/src/orchestration/mcp-tool-adapters.js +896 -0
- package/src/orchestration/reasoning/index.js +58 -0
- package/src/orchestration/reasoning/planning-engine.js +831 -0
- package/src/orchestration/reasoning/reasoning-engine.js +710 -0
- package/src/orchestration/reasoning/self-correction.js +751 -0
- package/src/orchestration/skill-executor.js +665 -0
- package/src/orchestration/skill-registry.js +650 -0
- package/src/orchestration/workflow-examples.js +1072 -0
- package/src/orchestration/workflow-executor.js +779 -0
- package/src/phase4-integration.js +248 -0
- package/src/phase5-integration.js +402 -0
- package/src/steering/steering-auto-update.js +572 -0
- package/src/steering/steering-validator.js +547 -0
- package/src/templates/template-constraints.js +646 -0
- package/src/validators/advanced-validation.js +580 -0
package/README.md
CHANGED
|
@@ -71,31 +71,36 @@ musubi init --windsurf # Windsurf IDE
|
|
|
71
71
|
|
|
72
72
|
---
|
|
73
73
|
|
|
74
|
-
## 📊 What's New in
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
-
|
|
78
|
-
- ✅ **
|
|
79
|
-
-
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
74
|
+
## 📊 What's New in v5.0.0
|
|
75
|
+
|
|
76
|
+
### Phase 5: Advanced Features 🚀
|
|
77
|
+
- 🔄 **Steering Auto-Update** - Automatic project memory synchronization with 5 trigger types
|
|
78
|
+
- ✅ **Steering Validator** - Comprehensive validation for steering documents
|
|
79
|
+
- 📋 **Template Constraints** - Enforce structure requirements with ThinkingChecklist
|
|
80
|
+
- 📊 **Quality Dashboard** - A-F grade metrics with health status monitoring
|
|
81
|
+
- 🔍 **Advanced Validation** - Cross-artifact consistency, gap detection, traceability validation
|
|
82
|
+
- 🎯 **Phase 5 Integration** - Unified access to all advanced features
|
|
83
|
+
|
|
84
|
+
### Phase 4: Agent Loop & Agentic Features 🤖
|
|
85
|
+
- 🧠 **Codebase Intelligence** - RepositoryMap, ASTExtractor, ContextOptimizer
|
|
86
|
+
- 💭 **Agentic Reasoning** - ReasoningEngine, PlanningEngine, SelfCorrection
|
|
87
|
+
- ⚡ **Code Generation** - CodeGenerator, CodeReviewer with multiple modes
|
|
88
|
+
- 🔗 **Integrated Agent** - Unified agent with all Phase 4 capabilities
|
|
83
89
|
|
|
84
90
|
```bash
|
|
85
|
-
#
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
# Output validation with PII redaction
|
|
89
|
-
npx musubi-validate guardrails "output" --type output --redact
|
|
91
|
+
# Use advanced features via orchestrator
|
|
92
|
+
const { Phase5Integration, createIntegratedAgent } = require('musubi-sdd');
|
|
90
93
|
|
|
91
|
-
#
|
|
92
|
-
|
|
94
|
+
# Quality dashboard
|
|
95
|
+
const dashboard = new QualityDashboard();
|
|
96
|
+
const metrics = await dashboard.collectMetrics(projectPath);
|
|
93
97
|
|
|
94
|
-
#
|
|
95
|
-
|
|
98
|
+
# Advanced validation
|
|
99
|
+
const validator = new AdvancedValidation();
|
|
100
|
+
const result = await validator.validateAll(projectPath);
|
|
96
101
|
```
|
|
97
102
|
|
|
98
|
-
### Previous (v3.
|
|
103
|
+
### Previous Highlights (v3.11.0)
|
|
99
104
|
|
|
100
105
|
- 🌐 **WebSocket Real-time GUI** - Live replanning updates with `musubi-browser` dashboard
|
|
101
106
|
- 📋 **GUI Quick Actions** - Modal dialog for New Requirement, Validate Project, Export Report
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "musubi-sdd",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "Ultimate Specification Driven Development Tool with 27 Agents for 7 AI Coding Platforms + MCP Integration (Claude Code, GitHub Copilot, Cursor, Gemini CLI, Windsurf, Codex, Qwen Code)",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MUSUBI Agent Loop
|
|
3
|
+
*
|
|
4
|
+
* Implements agentic tool-calling loop inspired by OpenAI Agents SDK
|
|
5
|
+
* and AutoGen patterns. Executes tool calls, processes results, and
|
|
6
|
+
* continues until completion or limit is reached.
|
|
7
|
+
*
|
|
8
|
+
* @module agents/agent-loop
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const EventEmitter = require('events');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} AgentLoopConfig
|
|
15
|
+
* @property {number} [maxIterations=10] - Maximum tool calling iterations
|
|
16
|
+
* @property {number} [timeout=60000] - Total timeout in milliseconds
|
|
17
|
+
* @property {number} [iterationTimeout=30000] - Per-iteration timeout
|
|
18
|
+
* @property {boolean} [continueOnError=false] - Continue on tool errors
|
|
19
|
+
* @property {Function} [completionCheck] - Custom completion checker
|
|
20
|
+
* @property {Object} [guardrails] - Input/output guardrails
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {Object} ToolDefinition
|
|
25
|
+
* @property {string} name - Tool name
|
|
26
|
+
* @property {string} description - Tool description
|
|
27
|
+
* @property {Object} parameters - JSON Schema for parameters
|
|
28
|
+
* @property {Function} handler - Async function to execute
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {Object} ToolCall
|
|
33
|
+
* @property {string} id - Unique call ID
|
|
34
|
+
* @property {string} tool - Tool name
|
|
35
|
+
* @property {Object} arguments - Tool arguments
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @typedef {Object} LoopResult
|
|
40
|
+
* @property {string} status - 'completed' | 'max_iterations' | 'timeout' | 'error'
|
|
41
|
+
* @property {Array} messages - Conversation history
|
|
42
|
+
* @property {Array} toolCalls - All tool calls made
|
|
43
|
+
* @property {*} finalOutput - Final result
|
|
44
|
+
* @property {Object} metrics - Execution metrics
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* AgentLoop class for managing tool-calling agentic workflows
|
|
49
|
+
*/
|
|
50
|
+
class AgentLoop extends EventEmitter {
|
|
51
|
+
/**
|
|
52
|
+
* @param {AgentLoopConfig} config
|
|
53
|
+
*/
|
|
54
|
+
constructor(config = {}) {
|
|
55
|
+
super();
|
|
56
|
+
|
|
57
|
+
this.maxIterations = config.maxIterations ?? 10;
|
|
58
|
+
this.timeout = config.timeout ?? 60000;
|
|
59
|
+
this.iterationTimeout = config.iterationTimeout ?? 30000;
|
|
60
|
+
this.continueOnError = config.continueOnError ?? false;
|
|
61
|
+
this.completionCheck = config.completionCheck ?? this.defaultCompletionCheck.bind(this);
|
|
62
|
+
this.guardrails = config.guardrails ?? null;
|
|
63
|
+
|
|
64
|
+
/** @type {Map<string, ToolDefinition>} */
|
|
65
|
+
this.tools = new Map();
|
|
66
|
+
|
|
67
|
+
/** @type {Array} */
|
|
68
|
+
this.messages = [];
|
|
69
|
+
|
|
70
|
+
/** @type {Array<ToolCall>} */
|
|
71
|
+
this.toolCallHistory = [];
|
|
72
|
+
|
|
73
|
+
/** @type {boolean} */
|
|
74
|
+
this.isRunning = false;
|
|
75
|
+
|
|
76
|
+
/** @type {AbortController|null} */
|
|
77
|
+
this.abortController = null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Register a tool for the agent to use
|
|
82
|
+
* @param {ToolDefinition} tool
|
|
83
|
+
*/
|
|
84
|
+
registerTool(tool) {
|
|
85
|
+
if (!tool.name || typeof tool.handler !== 'function') {
|
|
86
|
+
throw new Error('Tool must have name and handler function');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.tools.set(tool.name, {
|
|
90
|
+
name: tool.name,
|
|
91
|
+
description: tool.description || '',
|
|
92
|
+
parameters: tool.parameters || { type: 'object', properties: {} },
|
|
93
|
+
handler: tool.handler
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
this.emit('tool:registered', { name: tool.name });
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Register multiple tools at once
|
|
102
|
+
* @param {ToolDefinition[]} tools
|
|
103
|
+
*/
|
|
104
|
+
registerTools(tools) {
|
|
105
|
+
for (const tool of tools) {
|
|
106
|
+
this.registerTool(tool);
|
|
107
|
+
}
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Unregister a tool
|
|
113
|
+
* @param {string} name
|
|
114
|
+
*/
|
|
115
|
+
unregisterTool(name) {
|
|
116
|
+
const deleted = this.tools.delete(name);
|
|
117
|
+
if (deleted) {
|
|
118
|
+
this.emit('tool:unregistered', { name });
|
|
119
|
+
}
|
|
120
|
+
return deleted;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get all registered tools in OpenAI function format
|
|
125
|
+
* @returns {Array}
|
|
126
|
+
*/
|
|
127
|
+
getToolSchemas() {
|
|
128
|
+
return Array.from(this.tools.values()).map(tool => ({
|
|
129
|
+
type: 'function',
|
|
130
|
+
function: {
|
|
131
|
+
name: tool.name,
|
|
132
|
+
description: tool.description,
|
|
133
|
+
parameters: tool.parameters
|
|
134
|
+
}
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Run the agent loop with an LLM provider
|
|
140
|
+
* @param {Object} options
|
|
141
|
+
* @param {Object} options.llmProvider - LLM provider with chat() method
|
|
142
|
+
* @param {string} options.systemPrompt - System prompt for the agent
|
|
143
|
+
* @param {string} options.userMessage - Initial user message
|
|
144
|
+
* @param {Object} [options.context] - Additional context
|
|
145
|
+
* @returns {Promise<LoopResult>}
|
|
146
|
+
*/
|
|
147
|
+
async run({ llmProvider, systemPrompt, userMessage, context = {} }) {
|
|
148
|
+
if (this.isRunning) {
|
|
149
|
+
throw new Error('Agent loop is already running');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.isRunning = true;
|
|
153
|
+
this.abortController = new AbortController();
|
|
154
|
+
this.messages = [];
|
|
155
|
+
this.toolCallHistory = [];
|
|
156
|
+
|
|
157
|
+
const startTime = Date.now();
|
|
158
|
+
const timeoutId = setTimeout(() => this.abort('timeout'), this.timeout);
|
|
159
|
+
|
|
160
|
+
const metrics = {
|
|
161
|
+
iterations: 0,
|
|
162
|
+
toolCalls: 0,
|
|
163
|
+
startTime,
|
|
164
|
+
endTime: null,
|
|
165
|
+
duration: null,
|
|
166
|
+
tokensUsed: 0
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
// Initialize messages
|
|
171
|
+
this.messages.push({ role: 'system', content: systemPrompt });
|
|
172
|
+
|
|
173
|
+
// Apply input guardrails if configured
|
|
174
|
+
const processedInput = await this.applyInputGuardrails(userMessage, context);
|
|
175
|
+
this.messages.push({ role: 'user', content: processedInput });
|
|
176
|
+
|
|
177
|
+
let iteration = 0;
|
|
178
|
+
let completed = false;
|
|
179
|
+
let finalOutput = null;
|
|
180
|
+
|
|
181
|
+
while (iteration < this.maxIterations && !completed) {
|
|
182
|
+
if (this.abortController.signal.aborted) {
|
|
183
|
+
return this.createResult('aborted', finalOutput, metrics);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
iteration++;
|
|
187
|
+
metrics.iterations = iteration;
|
|
188
|
+
this.emit('iteration:start', { iteration, maxIterations: this.maxIterations });
|
|
189
|
+
|
|
190
|
+
// Call LLM
|
|
191
|
+
const response = await this.callLLMWithTimeout(llmProvider, {
|
|
192
|
+
messages: this.messages,
|
|
193
|
+
tools: this.getToolSchemas(),
|
|
194
|
+
context
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (response.usage) {
|
|
198
|
+
metrics.tokensUsed += response.usage.total_tokens || 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check for tool calls
|
|
202
|
+
if (response.toolCalls && response.toolCalls.length > 0) {
|
|
203
|
+
// Execute tool calls
|
|
204
|
+
const toolResults = await this.executeToolCalls(response.toolCalls);
|
|
205
|
+
metrics.toolCalls += response.toolCalls.length;
|
|
206
|
+
|
|
207
|
+
// Add assistant message with tool calls
|
|
208
|
+
this.messages.push({
|
|
209
|
+
role: 'assistant',
|
|
210
|
+
content: response.content || null,
|
|
211
|
+
tool_calls: response.toolCalls.map(tc => ({
|
|
212
|
+
id: tc.id,
|
|
213
|
+
type: 'function',
|
|
214
|
+
function: {
|
|
215
|
+
name: tc.tool || tc.name,
|
|
216
|
+
arguments: JSON.stringify(tc.arguments)
|
|
217
|
+
}
|
|
218
|
+
}))
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Add tool results
|
|
222
|
+
for (const result of toolResults) {
|
|
223
|
+
this.messages.push({
|
|
224
|
+
role: 'tool',
|
|
225
|
+
tool_call_id: result.callId,
|
|
226
|
+
content: JSON.stringify(result.output)
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
this.emit('iteration:complete', {
|
|
231
|
+
iteration,
|
|
232
|
+
toolCalls: response.toolCalls.length,
|
|
233
|
+
hasMore: true
|
|
234
|
+
});
|
|
235
|
+
} else {
|
|
236
|
+
// No tool calls - LLM response is final
|
|
237
|
+
finalOutput = response.content;
|
|
238
|
+
|
|
239
|
+
// Apply output guardrails
|
|
240
|
+
finalOutput = await this.applyOutputGuardrails(finalOutput, context);
|
|
241
|
+
|
|
242
|
+
// Add final assistant message
|
|
243
|
+
this.messages.push({
|
|
244
|
+
role: 'assistant',
|
|
245
|
+
content: finalOutput
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Check completion
|
|
249
|
+
completed = await this.completionCheck(finalOutput, this.messages, context);
|
|
250
|
+
|
|
251
|
+
this.emit('iteration:complete', {
|
|
252
|
+
iteration,
|
|
253
|
+
toolCalls: 0,
|
|
254
|
+
hasMore: !completed
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
metrics.endTime = Date.now();
|
|
260
|
+
metrics.duration = metrics.endTime - startTime;
|
|
261
|
+
|
|
262
|
+
if (iteration >= this.maxIterations && !completed) {
|
|
263
|
+
return this.createResult('max_iterations', finalOutput, metrics);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return this.createResult('completed', finalOutput, metrics);
|
|
267
|
+
|
|
268
|
+
} catch (error) {
|
|
269
|
+
metrics.endTime = Date.now();
|
|
270
|
+
metrics.duration = metrics.endTime - startTime;
|
|
271
|
+
|
|
272
|
+
this.emit('error', error);
|
|
273
|
+
|
|
274
|
+
if (error.message === 'timeout') {
|
|
275
|
+
return this.createResult('timeout', null, metrics);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return this.createResult('error', null, metrics, error);
|
|
279
|
+
} finally {
|
|
280
|
+
clearTimeout(timeoutId);
|
|
281
|
+
this.isRunning = false;
|
|
282
|
+
this.abortController = null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Call LLM with per-iteration timeout
|
|
288
|
+
* @param {Object} llmProvider
|
|
289
|
+
* @param {Object} options
|
|
290
|
+
* @returns {Promise<Object>}
|
|
291
|
+
*/
|
|
292
|
+
async callLLMWithTimeout(llmProvider, options) {
|
|
293
|
+
return new Promise((resolve, reject) => {
|
|
294
|
+
const timeoutId = setTimeout(() => {
|
|
295
|
+
reject(new Error('Iteration timeout'));
|
|
296
|
+
}, this.iterationTimeout);
|
|
297
|
+
|
|
298
|
+
llmProvider.chat(options)
|
|
299
|
+
.then(response => {
|
|
300
|
+
clearTimeout(timeoutId);
|
|
301
|
+
resolve(response);
|
|
302
|
+
})
|
|
303
|
+
.catch(error => {
|
|
304
|
+
clearTimeout(timeoutId);
|
|
305
|
+
reject(error);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Execute tool calls and collect results
|
|
312
|
+
* @param {ToolCall[]} toolCalls
|
|
313
|
+
* @returns {Promise<Array>}
|
|
314
|
+
*/
|
|
315
|
+
async executeToolCalls(toolCalls) {
|
|
316
|
+
const results = [];
|
|
317
|
+
|
|
318
|
+
for (const call of toolCalls) {
|
|
319
|
+
const toolName = call.tool || call.name;
|
|
320
|
+
const callId = call.id || `call_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
321
|
+
|
|
322
|
+
this.emit('tool:call', { name: toolName, arguments: call.arguments, id: callId });
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const tool = this.tools.get(toolName);
|
|
326
|
+
|
|
327
|
+
if (!tool) {
|
|
328
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const output = await tool.handler(call.arguments);
|
|
332
|
+
|
|
333
|
+
this.toolCallHistory.push({
|
|
334
|
+
id: callId,
|
|
335
|
+
tool: toolName,
|
|
336
|
+
arguments: call.arguments,
|
|
337
|
+
output,
|
|
338
|
+
status: 'success'
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
results.push({
|
|
342
|
+
callId,
|
|
343
|
+
tool: toolName,
|
|
344
|
+
output,
|
|
345
|
+
status: 'success'
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
this.emit('tool:result', { name: toolName, output, id: callId, status: 'success' });
|
|
349
|
+
|
|
350
|
+
} catch (error) {
|
|
351
|
+
const errorResult = {
|
|
352
|
+
callId,
|
|
353
|
+
tool: toolName,
|
|
354
|
+
error: error.message,
|
|
355
|
+
status: 'error'
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
this.toolCallHistory.push({
|
|
359
|
+
id: callId,
|
|
360
|
+
tool: toolName,
|
|
361
|
+
arguments: call.arguments,
|
|
362
|
+
error: error.message,
|
|
363
|
+
status: 'error'
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
this.emit('tool:error', { name: toolName, error: error.message, id: callId });
|
|
367
|
+
|
|
368
|
+
if (this.continueOnError) {
|
|
369
|
+
results.push({
|
|
370
|
+
callId,
|
|
371
|
+
tool: toolName,
|
|
372
|
+
output: { error: error.message },
|
|
373
|
+
status: 'error'
|
|
374
|
+
});
|
|
375
|
+
} else {
|
|
376
|
+
throw error;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return results;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Default completion check - returns true when LLM provides final response
|
|
386
|
+
* @param {*} output
|
|
387
|
+
* @param {Array} messages
|
|
388
|
+
* @param {Object} context
|
|
389
|
+
* @returns {Promise<boolean>}
|
|
390
|
+
*/
|
|
391
|
+
async defaultCompletionCheck(output, messages, context) {
|
|
392
|
+
// By default, if we reach here (no tool calls), we're done
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Apply input guardrails if configured
|
|
398
|
+
* @param {string} input
|
|
399
|
+
* @param {Object} context
|
|
400
|
+
* @returns {Promise<string>}
|
|
401
|
+
*/
|
|
402
|
+
async applyInputGuardrails(input, context) {
|
|
403
|
+
if (!this.guardrails?.input) {
|
|
404
|
+
return input;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
const result = await this.guardrails.input.validate(input, context);
|
|
409
|
+
if (!result.valid) {
|
|
410
|
+
throw new Error(`Input guardrail violation: ${result.reason}`);
|
|
411
|
+
}
|
|
412
|
+
return result.transformed || input;
|
|
413
|
+
} catch (error) {
|
|
414
|
+
this.emit('guardrail:input:error', error);
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Apply output guardrails if configured
|
|
421
|
+
* @param {string} output
|
|
422
|
+
* @param {Object} context
|
|
423
|
+
* @returns {Promise<string>}
|
|
424
|
+
*/
|
|
425
|
+
async applyOutputGuardrails(output, context) {
|
|
426
|
+
if (!this.guardrails?.output) {
|
|
427
|
+
return output;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
const result = await this.guardrails.output.validate(output, context);
|
|
432
|
+
if (!result.valid) {
|
|
433
|
+
this.emit('guardrail:output:violation', { output, reason: result.reason });
|
|
434
|
+
return result.fallback || '[Output blocked by guardrails]';
|
|
435
|
+
}
|
|
436
|
+
return result.transformed || output;
|
|
437
|
+
} catch (error) {
|
|
438
|
+
this.emit('guardrail:output:error', error);
|
|
439
|
+
return output;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Create standardized result object
|
|
445
|
+
* @param {string} status
|
|
446
|
+
* @param {*} finalOutput
|
|
447
|
+
* @param {Object} metrics
|
|
448
|
+
* @param {Error} [error]
|
|
449
|
+
* @returns {LoopResult}
|
|
450
|
+
*/
|
|
451
|
+
createResult(status, finalOutput, metrics, error = null) {
|
|
452
|
+
return {
|
|
453
|
+
status,
|
|
454
|
+
messages: this.messages,
|
|
455
|
+
toolCalls: this.toolCallHistory,
|
|
456
|
+
finalOutput,
|
|
457
|
+
metrics: {
|
|
458
|
+
...metrics,
|
|
459
|
+
toolCallCount: this.toolCallHistory.length,
|
|
460
|
+
successfulCalls: this.toolCallHistory.filter(tc => tc.status === 'success').length,
|
|
461
|
+
failedCalls: this.toolCallHistory.filter(tc => tc.status === 'error').length
|
|
462
|
+
},
|
|
463
|
+
...(error && { error: error.message })
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Abort the running loop
|
|
469
|
+
* @param {string} [reason='aborted']
|
|
470
|
+
*/
|
|
471
|
+
abort(reason = 'aborted') {
|
|
472
|
+
if (this.abortController) {
|
|
473
|
+
this.abortController.abort();
|
|
474
|
+
this.emit('abort', { reason });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Get current loop state
|
|
480
|
+
* @returns {Object}
|
|
481
|
+
*/
|
|
482
|
+
getState() {
|
|
483
|
+
return {
|
|
484
|
+
isRunning: this.isRunning,
|
|
485
|
+
messageCount: this.messages.length,
|
|
486
|
+
toolCallCount: this.toolCallHistory.length,
|
|
487
|
+
registeredTools: Array.from(this.tools.keys())
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Reset the loop state
|
|
493
|
+
*/
|
|
494
|
+
reset() {
|
|
495
|
+
if (this.isRunning) {
|
|
496
|
+
throw new Error('Cannot reset while loop is running');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
this.messages = [];
|
|
500
|
+
this.toolCallHistory = [];
|
|
501
|
+
this.emit('reset');
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Create a simple mock LLM provider for testing
|
|
507
|
+
* @param {Array} responses - Pre-defined responses
|
|
508
|
+
* @returns {Object}
|
|
509
|
+
*/
|
|
510
|
+
function createMockLLMProvider(responses) {
|
|
511
|
+
let callIndex = 0;
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
async chat(options) {
|
|
515
|
+
if (callIndex >= responses.length) {
|
|
516
|
+
return { content: 'Done', toolCalls: [] };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const response = responses[callIndex++];
|
|
520
|
+
return {
|
|
521
|
+
content: response.content || null,
|
|
522
|
+
toolCalls: response.toolCalls || [],
|
|
523
|
+
usage: response.usage || { total_tokens: 100 }
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
module.exports = {
|
|
530
|
+
AgentLoop,
|
|
531
|
+
createMockLLMProvider
|
|
532
|
+
};
|