@backendkit-labs/agent-core 0.20.1 → 0.21.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 +3 -0
- package/dist/commands/SlashCommandRegistry.d.ts +7 -2
- package/dist/commands/SlashCommandRegistry.d.ts.map +1 -1
- package/dist/commands/SlashCommandRegistry.js.map +1 -1
- package/dist/commands/builtin.js +4 -4
- package/dist/commands/builtin.js.map +1 -1
- package/dist/delegation/DelegationBus.d.ts +15 -7
- package/dist/delegation/DelegationBus.d.ts.map +1 -1
- package/dist/delegation/DelegationBus.js +33 -12
- package/dist/delegation/DelegationBus.js.map +1 -1
- package/dist/engine/AgentEngine.d.ts +9 -45
- package/dist/engine/AgentEngine.d.ts.map +1 -1
- package/dist/engine/AgentEngine.js +130 -432
- package/dist/engine/AgentEngine.js.map +1 -1
- package/dist/engine/ApprovalGate.d.ts +33 -0
- package/dist/engine/ApprovalGate.d.ts.map +1 -0
- package/dist/engine/ApprovalGate.js +76 -0
- package/dist/engine/ApprovalGate.js.map +1 -0
- package/dist/engine/AuditLogger.d.ts +14 -0
- package/dist/engine/AuditLogger.d.ts.map +1 -0
- package/dist/engine/AuditLogger.js +34 -0
- package/dist/engine/AuditLogger.js.map +1 -0
- package/dist/engine/HistoryManager.d.ts +42 -0
- package/dist/engine/HistoryManager.d.ts.map +1 -0
- package/dist/engine/HistoryManager.js +156 -0
- package/dist/engine/HistoryManager.js.map +1 -0
- package/dist/engine/MCPRegistrar.d.ts +44 -0
- package/dist/engine/MCPRegistrar.d.ts.map +1 -0
- package/dist/engine/MCPRegistrar.js +209 -0
- package/dist/engine/MCPRegistrar.js.map +1 -0
- package/dist/engine/qa-orchestrator.js +2 -2
- package/dist/engine/qa-orchestrator.js.map +1 -1
- package/dist/mcp/MCPClientManager.d.ts +7 -0
- package/dist/mcp/MCPClientManager.d.ts.map +1 -1
- package/dist/mcp/MCPClientManager.js +38 -7
- package/dist/mcp/MCPClientManager.js.map +1 -1
- package/dist/qa/qa-service.d.ts.map +1 -1
- package/dist/qa/qa-service.js +43 -34
- package/dist/qa/qa-service.js.map +1 -1
- package/dist/reflection/lessons-memo-generator.d.ts +1 -1
- package/dist/reflection/lessons-memo-generator.js +64 -64
- package/dist/reflection/lessons-memo-generator.js.map +1 -1
- package/package.json +87 -87
|
@@ -1,59 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.AgentEngine = void 0;
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
4
|
+
const ApprovalGate_1 = require("./ApprovalGate");
|
|
5
|
+
const AuditLogger_1 = require("./AuditLogger");
|
|
6
|
+
const HistoryManager_1 = require("./HistoryManager");
|
|
7
|
+
const MCPRegistrar_1 = require("./MCPRegistrar");
|
|
7
8
|
const DelegationBus_1 = require("../delegation/DelegationBus");
|
|
8
9
|
const SessionMemory_1 = require("../memory/SessionMemory");
|
|
9
10
|
const activator_1 = require("../skills/activator");
|
|
10
11
|
const WorkflowRunner_1 = require("../workflow/WorkflowRunner");
|
|
11
12
|
const IterationManager_1 = require("./IterationManager");
|
|
12
13
|
const qa_orchestrator_1 = require("./qa-orchestrator");
|
|
13
|
-
/**
|
|
14
|
-
* Remove messages that would cause a 400 from any OpenAI-compatible provider:
|
|
15
|
-
* - `tool` messages that are not part of an active assistant→tool chain
|
|
16
|
-
* - `assistant` messages that declare tool_calls but have no following tool results
|
|
17
|
-
* (only stripped when the next non-tool message is NOT another turn of the same chain)
|
|
18
|
-
*
|
|
19
|
-
* Walking forward: we track whether we're "inside" a tool_calls chain.
|
|
20
|
-
* A chain starts with assistant(tool_calls) and ends once all IDs have a result.
|
|
21
|
-
* Any `tool` result that arrives outside an open chain is dropped.
|
|
22
|
-
*/
|
|
23
|
-
function sanitizeMessages(messages) {
|
|
24
|
-
const out = [];
|
|
25
|
-
// IDs still waiting for a tool result in the current chain
|
|
26
|
-
let pendingIds = new Set();
|
|
27
|
-
for (const msg of messages) {
|
|
28
|
-
if (msg.role === 'assistant') {
|
|
29
|
-
// A new assistant message closes any previous chain (even if incomplete)
|
|
30
|
-
// and optionally opens a new one.
|
|
31
|
-
pendingIds = new Set();
|
|
32
|
-
if (msg.tool_calls?.length) {
|
|
33
|
-
for (const tc of msg.tool_calls) {
|
|
34
|
-
if (tc.id)
|
|
35
|
-
pendingIds.add(tc.id);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
out.push(msg);
|
|
39
|
-
}
|
|
40
|
-
else if (msg.role === 'tool') {
|
|
41
|
-
if (pendingIds.size === 0) {
|
|
42
|
-
// Orphaned tool result — no open chain. Drop it.
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
if (msg.tool_call_id)
|
|
46
|
-
pendingIds.delete(msg.tool_call_id);
|
|
47
|
-
out.push(msg);
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
// user / system — close any open chain
|
|
51
|
-
pendingIds = new Set();
|
|
52
|
-
out.push(msg);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return out;
|
|
56
|
-
}
|
|
57
14
|
const MAX_INPUT_BYTES = 100_000; // Fix #17: 100 KB hard cap on user input
|
|
58
15
|
const ASK_AGENT_TOOL = {
|
|
59
16
|
name: 'ask_agent',
|
|
@@ -76,70 +33,54 @@ class AgentEngine {
|
|
|
76
33
|
memory;
|
|
77
34
|
bus;
|
|
78
35
|
skillActivator = new activator_1.SkillActivator();
|
|
36
|
+
history;
|
|
37
|
+
auditLogger;
|
|
79
38
|
currentAgentId;
|
|
80
|
-
messages = [];
|
|
81
39
|
aborted = false;
|
|
82
40
|
abortController = null;
|
|
83
41
|
allSkills;
|
|
84
42
|
iterationManager;
|
|
85
43
|
activeSkillAddition = '';
|
|
86
44
|
yamlSkillsLoaded = false;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
mcpRegisteredServers = new Set();
|
|
90
|
-
mcpTriggers = new Map(); // agentId → triggers
|
|
91
|
-
mcpToolTriggers = new Map(); // keyword → {agentId, tool}
|
|
92
|
-
approvalDisabled = false; // set to true after 'approve_all'
|
|
45
|
+
mcpRegistrar;
|
|
46
|
+
approvalGate;
|
|
93
47
|
pendingContextLoad = null;
|
|
94
|
-
static WRITE_TOOLS = new Set(['write_file', 'edit_file', 'run_command']);
|
|
95
|
-
/** Tracks what each MCP server blocked/assigned so it can be fully reverted on disconnect. */
|
|
96
|
-
mcpAgentBlocks = new Map();
|
|
97
|
-
/**
|
|
98
|
-
* Commands that always require user approval regardless of iteration mode or
|
|
99
|
-
* approvalDisabled flag. These are irreversible or high-blast-radius operations:
|
|
100
|
-
* installing new packages, removing packages, running destructive npm scripts.
|
|
101
|
-
*/
|
|
102
|
-
static ALWAYS_ASK_PATTERNS = [
|
|
103
|
-
/^\s*npm\s+(install|i|add)\s+[^-]/i, // npm install <pkg> — new package install
|
|
104
|
-
/^\s*npm\s+uninstall\b/i, // npm uninstall
|
|
105
|
-
/^\s*yarn\s+add\s+[^-]/i, // yarn add <pkg>
|
|
106
|
-
/^\s*pnpm\s+add\s+[^-]/i, // pnpm add <pkg>
|
|
107
|
-
/^\s*pip\s+install\b/i, // pip install
|
|
108
|
-
/^\s*pip3\s+install\b/i,
|
|
109
|
-
];
|
|
110
48
|
agentsLoaded = false;
|
|
49
|
+
agentsLoadedPromise = null;
|
|
111
50
|
memoryContextBlock = '';
|
|
51
|
+
/** Serializes concurrent run() calls — shared mutable state is not reentrant-safe. */
|
|
52
|
+
runMutex = Promise.resolve();
|
|
112
53
|
constructor(opts) {
|
|
113
54
|
this.opts = opts;
|
|
114
55
|
this.memory = opts.sessionMemory ?? new SessionMemory_1.SessionMemory();
|
|
115
56
|
this.currentAgentId = opts.defaultAgentId;
|
|
116
57
|
this.allSkills = opts.skills ?? [];
|
|
58
|
+
this.history = new HistoryManager_1.HistoryManager({
|
|
59
|
+
historyPath: opts.historyPath,
|
|
60
|
+
maxContextMessages: opts.maxContextMessages,
|
|
61
|
+
});
|
|
62
|
+
this.auditLogger = new AuditLogger_1.AuditLogger(opts.auditLog);
|
|
63
|
+
this.mcpRegistrar = new MCPRegistrar_1.MCPRegistrar({
|
|
64
|
+
mcpManager: opts.mcpManager,
|
|
65
|
+
tools: opts.tools,
|
|
66
|
+
agents: opts.agents,
|
|
67
|
+
transport: opts.transport,
|
|
68
|
+
});
|
|
69
|
+
this.approvalGate = new ApprovalGate_1.ApprovalGate({
|
|
70
|
+
onToolApproval: opts.onToolApproval,
|
|
71
|
+
transport: opts.transport,
|
|
72
|
+
});
|
|
117
73
|
this.iterationManager = new IterationManager_1.IterationManager({
|
|
118
74
|
mode: opts.iterationMode ?? 'interactive',
|
|
119
75
|
maxIterations: opts.maxIterations ?? 100,
|
|
120
76
|
onLimitReached: opts.onIterationLimit,
|
|
121
77
|
onStep: opts.onStep,
|
|
122
78
|
});
|
|
123
|
-
if (opts.auditLog) {
|
|
124
|
-
try {
|
|
125
|
-
(0, fs_1.mkdirSync)((0, path_1.dirname)(opts.auditLog), { recursive: true });
|
|
126
|
-
}
|
|
127
|
-
catch { }
|
|
128
|
-
}
|
|
129
|
-
if (opts.historyPath && (0, fs_1.existsSync)(opts.historyPath)) {
|
|
130
|
-
try {
|
|
131
|
-
const raw = JSON.parse((0, fs_1.readFileSync)(opts.historyPath, 'utf-8'));
|
|
132
|
-
this.messages = raw.filter(m => m.role !== 'system');
|
|
133
|
-
}
|
|
134
|
-
catch { /* corrupt or missing — start fresh */ }
|
|
135
|
-
}
|
|
136
79
|
this.bus = new DelegationBus_1.DelegationBus({
|
|
137
80
|
maxParallel: opts.maxParallelAgents ?? 6,
|
|
138
81
|
agents: opts.agents,
|
|
139
|
-
tools: opts.tools,
|
|
140
82
|
transport: opts.transport,
|
|
141
|
-
|
|
142
|
-
runAgent: (id, question, context) => this.runAgentInternal(id, question, context),
|
|
83
|
+
runAgent: (id, question, context, depth, buf) => this.runAgentInternal(id, question, context, depth, buf),
|
|
143
84
|
});
|
|
144
85
|
}
|
|
145
86
|
/** Access the observability manager when configured via createBaseEngine({ observability }). */
|
|
@@ -179,7 +120,7 @@ class AgentEngine {
|
|
|
179
120
|
* without requiring a full engine restart.
|
|
180
121
|
*/
|
|
181
122
|
injectContext(message) {
|
|
182
|
-
this.
|
|
123
|
+
this.history.inject(message);
|
|
183
124
|
}
|
|
184
125
|
/**
|
|
185
126
|
* Returns a partial CommandContext pre-wired to this engine instance.
|
|
@@ -193,7 +134,11 @@ class AgentEngine {
|
|
|
193
134
|
*/
|
|
194
135
|
getCommandBindings() {
|
|
195
136
|
return {
|
|
196
|
-
onCwdChange: (cwd) =>
|
|
137
|
+
onCwdChange: (cwd, contextMessage) => {
|
|
138
|
+
this.updateWorkingDir(cwd);
|
|
139
|
+
if (contextMessage)
|
|
140
|
+
this.injectContext(contextMessage);
|
|
141
|
+
},
|
|
197
142
|
injectContext: (message) => this.injectContext(message),
|
|
198
143
|
setIterationMode: (mode) => this.setIterationMode(mode),
|
|
199
144
|
getIterationMode: () => this.getIterationMode(),
|
|
@@ -245,6 +190,21 @@ class AgentEngine {
|
|
|
245
190
|
return this.opts.providers.resolve(profile.provider, this.opts.defaultProvider);
|
|
246
191
|
}
|
|
247
192
|
async run(input) {
|
|
193
|
+
// Serialize concurrent calls — messages, currentAgentId, and other fields are
|
|
194
|
+
// shared mutable state that is not safe for concurrent access.
|
|
195
|
+
let release;
|
|
196
|
+
const slot = new Promise(r => { release = r; });
|
|
197
|
+
const prev = this.runMutex;
|
|
198
|
+
this.runMutex = prev.then(() => slot);
|
|
199
|
+
await prev;
|
|
200
|
+
try {
|
|
201
|
+
await this._run(input);
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
release();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async _run(input) {
|
|
248
208
|
if (this.pendingContextLoad)
|
|
249
209
|
await this.pendingContextLoad;
|
|
250
210
|
this.aborted = false;
|
|
@@ -258,27 +218,27 @@ class AgentEngine {
|
|
|
258
218
|
this.opts.transport.emit({ type: 'done' });
|
|
259
219
|
return;
|
|
260
220
|
}
|
|
261
|
-
await this.
|
|
221
|
+
await this.mcpRegistrar.ensureInitialized();
|
|
262
222
|
await this.ensureAgentsLoaded();
|
|
263
223
|
// Direct MCP routing: bypass general when input matches a configured trigger
|
|
264
|
-
const mcpRoute = this.
|
|
224
|
+
const mcpRoute = this.mcpRegistrar.findRoute(input);
|
|
265
225
|
if (mcpRoute) {
|
|
266
226
|
if (mcpRoute.tool) {
|
|
267
|
-
// Tool trigger: call the MCP tool directly — no intermediate LLM
|
|
227
|
+
// Tool trigger: call the MCP tool directly — no intermediate LLM.
|
|
228
|
+
// Route through executeToolCall so auth checks and onToolApproval gate apply.
|
|
268
229
|
const toolName = `mcp__${mcpRoute.agentId}__${mcpRoute.tool}`;
|
|
269
230
|
const toolDef = this.opts.tools.get(toolName);
|
|
270
231
|
if (toolDef) {
|
|
271
232
|
const required = toolDef.parameters.required;
|
|
272
233
|
const firstParam = required?.[0] ?? 'input';
|
|
273
|
-
const
|
|
274
|
-
const profile = this.opts.agents.get(mcpRoute.agentId);
|
|
234
|
+
const mcpProfile = this.opts.agents.get(mcpRoute.agentId) ?? this.getCurrentProfile();
|
|
275
235
|
this.opts.transport.emit({
|
|
276
236
|
type: 'block_start',
|
|
277
237
|
agent_id: mcpRoute.agentId,
|
|
278
|
-
agent_name:
|
|
279
|
-
agent_icon:
|
|
238
|
+
agent_name: mcpProfile.name ?? mcpRoute.agentId,
|
|
239
|
+
agent_icon: mcpProfile.icon ?? '◎',
|
|
280
240
|
});
|
|
281
|
-
const result = await
|
|
241
|
+
const result = await this.executeToolCall({ name: toolName, args: JSON.stringify({ [firstParam]: input }) }, mcpProfile);
|
|
282
242
|
this.opts.transport.emit({ type: 'token', content: result, agent_id: mcpRoute.agentId });
|
|
283
243
|
this.opts.transport.emit({ type: 'block_end', status: 'ok', agent_id: mcpRoute.agentId });
|
|
284
244
|
this.opts.transport.emit({ type: 'done' });
|
|
@@ -316,7 +276,7 @@ class AgentEngine {
|
|
|
316
276
|
if (this.opts.orchestrator) {
|
|
317
277
|
orchestrationResult = await this.opts.orchestrator.orchestrate(input).catch(() => undefined);
|
|
318
278
|
}
|
|
319
|
-
this.
|
|
279
|
+
this.history.push({ role: 'user', content: input });
|
|
320
280
|
await this.runLoop(profile, orchestrationResult);
|
|
321
281
|
this.saveHistory();
|
|
322
282
|
this.opts.transport.emit({ type: 'done' });
|
|
@@ -324,12 +284,12 @@ class AgentEngine {
|
|
|
324
284
|
async runWorkflow(defOrBuilder, opts = {}) {
|
|
325
285
|
if (this.pendingContextLoad)
|
|
326
286
|
await this.pendingContextLoad;
|
|
327
|
-
await this.
|
|
287
|
+
await this.mcpRegistrar.ensureInitialized();
|
|
328
288
|
await this.ensureAgentsLoaded();
|
|
329
289
|
const def = 'build' in defOrBuilder ? defOrBuilder.build() : defOrBuilder;
|
|
330
290
|
const runner = new WorkflowRunner_1.WorkflowRunner(def, {
|
|
331
291
|
...opts,
|
|
332
|
-
runAgent: (agentId, input, context) => this.runAgentInternal(agentId, input, context),
|
|
292
|
+
runAgent: (agentId, input, context) => this.runAgentInternal(agentId, input, context, 0),
|
|
333
293
|
transport: this.opts.transport,
|
|
334
294
|
});
|
|
335
295
|
return runner.run();
|
|
@@ -349,59 +309,39 @@ class AgentEngine {
|
|
|
349
309
|
}
|
|
350
310
|
/** Delete persisted history file and reset in-memory conversation. */
|
|
351
311
|
clearHistory() {
|
|
352
|
-
this.
|
|
353
|
-
if (!this.opts.historyPath)
|
|
354
|
-
return;
|
|
355
|
-
try {
|
|
356
|
-
(0, atomic_write_1.atomicWriteSync)(this.opts.historyPath, '[]');
|
|
357
|
-
}
|
|
358
|
-
catch { /* non-fatal */ }
|
|
312
|
+
this.history.clear();
|
|
359
313
|
}
|
|
360
|
-
/** Serialize current conversation to disk (called automatically after each run). */
|
|
361
314
|
saveHistory() {
|
|
362
|
-
|
|
363
|
-
return;
|
|
364
|
-
const cap = this.opts.maxContextMessages ?? 100;
|
|
365
|
-
const toSave = this.messages.filter(m => m.role !== 'system').slice(-cap);
|
|
366
|
-
try {
|
|
367
|
-
(0, fs_1.mkdirSync)((0, path_1.dirname)(this.opts.historyPath), { recursive: true });
|
|
368
|
-
(0, atomic_write_1.atomicWriteSync)(this.opts.historyPath, JSON.stringify(toSave, null, 2));
|
|
369
|
-
}
|
|
370
|
-
catch { /* non-fatal — history loss is preferable to a crash */ }
|
|
315
|
+
this.history.save();
|
|
371
316
|
}
|
|
372
|
-
/**
|
|
373
|
-
* Trim conversation history to maxContextMessages by dropping oldest messages
|
|
374
|
-
* at a user-turn boundary. Preserves all assistant↔tool pairs intact.
|
|
375
|
-
*/
|
|
376
317
|
trimHistory() {
|
|
377
318
|
const max = this.opts.maxContextMessages;
|
|
378
|
-
if (!max
|
|
319
|
+
if (!max)
|
|
379
320
|
return;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const target = this.messages.length - max;
|
|
383
|
-
let cutAt = target;
|
|
384
|
-
while (cutAt < this.messages.length && this.messages[cutAt].role !== 'user') {
|
|
385
|
-
cutAt++;
|
|
386
|
-
}
|
|
387
|
-
if (cutAt > 0 && cutAt < this.messages.length) {
|
|
388
|
-
this.messages = this.messages.slice(cutAt);
|
|
321
|
+
const dropped = this.history.trim(max);
|
|
322
|
+
if (dropped > 0) {
|
|
389
323
|
this.opts.transport.emit({
|
|
390
324
|
type: 'system',
|
|
391
325
|
level: 'warn',
|
|
392
|
-
text: `[ContextWindow] Dropped ${
|
|
326
|
+
text: `[ContextWindow] Dropped ${dropped} messages — history capped at ${max} (maxContextMessages)`,
|
|
393
327
|
});
|
|
394
328
|
}
|
|
395
329
|
}
|
|
396
330
|
async ensureAgentsLoaded() {
|
|
397
331
|
if (this.agentsLoaded || !this.opts.loadAgents)
|
|
398
332
|
return;
|
|
399
|
-
|
|
333
|
+
// Cache the promise so concurrent run() calls wait for the same load,
|
|
334
|
+
// rather than the early-flag pattern that let callers proceed unguarded.
|
|
335
|
+
if (this.agentsLoadedPromise)
|
|
336
|
+
return this.agentsLoadedPromise;
|
|
337
|
+
this.agentsLoadedPromise = this._loadAgentsOnce();
|
|
338
|
+
return this.agentsLoadedPromise;
|
|
339
|
+
}
|
|
340
|
+
async _loadAgentsOnce() {
|
|
400
341
|
try {
|
|
401
342
|
const discovered = await this.opts.loadAgents();
|
|
402
343
|
for (const agent of discovered)
|
|
403
344
|
this.opts.agents.upsert(agent);
|
|
404
|
-
// If the current default agent still isn't registered, fall back to first available
|
|
405
345
|
if (!this.opts.agents.has(this.currentAgentId)) {
|
|
406
346
|
const all = this.opts.agents.getAll();
|
|
407
347
|
if (all.length > 0)
|
|
@@ -414,175 +354,10 @@ class AgentEngine {
|
|
|
414
354
|
text: `[AgentDiscovery] Failed to load agents: ${err.message}`,
|
|
415
355
|
});
|
|
416
356
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
if (!this.opts.mcpManager)
|
|
420
|
-
return;
|
|
421
|
-
try {
|
|
422
|
-
if (!this.mcpInitialized) {
|
|
423
|
-
await this.opts.mcpManager.initialize();
|
|
424
|
-
this.mcpInitialized = true;
|
|
425
|
-
}
|
|
426
|
-
else {
|
|
427
|
-
// Subsequent runs: retry only servers that are still offline — bounded by their own counter
|
|
428
|
-
const offline = this.opts.mcpManager.getConfiguredNames()
|
|
429
|
-
.filter(n => !this.mcpRegisteredServers.has(n));
|
|
430
|
-
if (offline.length > 0 && this.mcpInitAttempts < 3) {
|
|
431
|
-
this.mcpInitAttempts++;
|
|
432
|
-
await this.opts.mcpManager.reconnectFailed();
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
catch (err) {
|
|
437
|
-
this.opts.transport.emit({
|
|
438
|
-
type: 'system', level: 'warn',
|
|
439
|
-
text: `MCP initialization error: ${err.message}`,
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
// Always register any server that is now connected but not yet registered as agent.
|
|
443
|
-
// This runs regardless of the retry counter so late-starting servers get picked up.
|
|
444
|
-
for (const { name, toolCount } of this.opts.mcpManager.getServerInfo()) {
|
|
445
|
-
if (toolCount > 0 && !this.mcpRegisteredServers.has(name)) {
|
|
446
|
-
const tools = this.opts.mcpManager.getServerTools(name);
|
|
447
|
-
const config = this.opts.mcpManager.getConfig(name);
|
|
448
|
-
this.registerMCPAgent(config, tools);
|
|
449
|
-
this.mcpRegisteredServers.add(name);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
// Warn about servers still offline (only while retries remain)
|
|
453
|
-
const stillOffline = this.opts.mcpManager.getConfiguredNames()
|
|
454
|
-
.filter(n => !this.mcpRegisteredServers.has(n));
|
|
455
|
-
if (stillOffline.length > 0) {
|
|
456
|
-
const retriesLeft = 3 - this.mcpInitAttempts;
|
|
457
|
-
const suffix = retriesLeft > 0
|
|
458
|
-
? `Retrying on next message (${retriesLeft} attempts left).`
|
|
459
|
-
: `No more automatic retries — call engine.getMCPManager()?.reconnectFailed() to retry manually.`;
|
|
460
|
-
this.opts.transport.emit({
|
|
461
|
-
type: 'system', level: 'warn',
|
|
462
|
-
text: `MCP agents offline: ${stillOffline.join(', ')} — start their servers to enable them. ${suffix}`,
|
|
463
|
-
});
|
|
357
|
+
finally {
|
|
358
|
+
this.agentsLoaded = true;
|
|
464
359
|
}
|
|
465
360
|
}
|
|
466
|
-
registerMCPAgent(config, tools) {
|
|
467
|
-
// safeName mirrors the sanitization done by MCPClientManager.wrapTool() so that
|
|
468
|
-
// tool lookup `mcp__${safeName}__${tool}` always resolves correctly.
|
|
469
|
-
const safeName = config.name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
470
|
-
// Register tools in the ToolRegistry — skip already-registered ones
|
|
471
|
-
// (safe for reconnection scenarios; mcpRegisteredServers normally prevents duplicates)
|
|
472
|
-
for (const tool of tools) {
|
|
473
|
-
if (!this.opts.tools.has(tool.name)) {
|
|
474
|
-
this.opts.tools.register(tool);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
// toolTriggers work in both modes — direct keyword → tool execution, zero LLM hops
|
|
478
|
-
if (config.toolTriggers) {
|
|
479
|
-
for (const [keyword, tool] of Object.entries(config.toolTriggers)) {
|
|
480
|
-
this.mcpToolTriggers.set(keyword.toLowerCase(), { agentId: safeName, tool });
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
const mode = config.mode ?? 'tools';
|
|
484
|
-
if (mode === 'tools') {
|
|
485
|
-
this.registerMCPToolPack(config, tools, safeName);
|
|
486
|
-
}
|
|
487
|
-
else {
|
|
488
|
-
this.registerMCPAgentWrapper(config, tools, safeName);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
/**
|
|
492
|
-
* mode='tools' (default): assigns MCP tools directly to local agent allowedTools.
|
|
493
|
-
* Also applies blocksCommands to those agents while the server is connected —
|
|
494
|
-
* forcing them to use the MCP tools instead of equivalent shell commands.
|
|
495
|
-
* Both tool assignments and command blocks are tracked so they can be
|
|
496
|
-
* fully reverted when the server disconnects via unregisterMCPServer().
|
|
497
|
-
*/
|
|
498
|
-
registerMCPToolPack(config, tools, safeName) {
|
|
499
|
-
const toolNames = tools.map(t => t.name);
|
|
500
|
-
const blocksCommands = config.blocksCommands ?? [];
|
|
501
|
-
// Resolve target agents: explicit assignTo list, or all registered agents
|
|
502
|
-
const assignTo = config.assignTo?.filter(id => id !== '*') ?? [];
|
|
503
|
-
const targets = assignTo.length > 0
|
|
504
|
-
? assignTo
|
|
505
|
-
: this.opts.agents.getAll().map(a => a.id);
|
|
506
|
-
const blockRecord = [];
|
|
507
|
-
for (const agentId of targets) {
|
|
508
|
-
const agent = this.opts.agents.get(agentId);
|
|
509
|
-
if (!agent) {
|
|
510
|
-
this.opts.transport.emit({
|
|
511
|
-
type: 'system', level: 'warn',
|
|
512
|
-
text: `[MCP:${config.name}] assignTo agent "${agentId}" not found — skipped.`,
|
|
513
|
-
});
|
|
514
|
-
continue;
|
|
515
|
-
}
|
|
516
|
-
this.opts.agents.upsert({
|
|
517
|
-
...agent,
|
|
518
|
-
allowedTools: [...new Set([...agent.allowedTools, ...toolNames])],
|
|
519
|
-
blockedCommands: [...new Set([...(agent.blockedCommands ?? []), ...blocksCommands])],
|
|
520
|
-
});
|
|
521
|
-
blockRecord.push({ agentId, toolNames, blockedCmds: blocksCommands });
|
|
522
|
-
}
|
|
523
|
-
this.mcpAgentBlocks.set(safeName, blockRecord);
|
|
524
|
-
if (config.triggers?.length) {
|
|
525
|
-
this.opts.transport.emit({
|
|
526
|
-
type: 'system', level: 'warn',
|
|
527
|
-
text: `[MCP:${config.name}] 'triggers' is ignored in mode='tools'. Use 'toolTriggers' for direct keyword routing.`,
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
const shortNames = tools.map(t => t.name.split('__')[2] ?? t.name).join(', ');
|
|
531
|
-
const agentList = targets.join(', ') || 'all agents';
|
|
532
|
-
const blockedLabel = blocksCommands.length ? ` blocked: [${blocksCommands.join(', ')}]` : '';
|
|
533
|
-
this.opts.transport.emit({
|
|
534
|
-
type: 'system', level: 'info',
|
|
535
|
-
text: `[MCP:${config.name}] tools [${shortNames}] assigned to: ${agentList}${blockedLabel}`,
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
/**
|
|
539
|
-
* mode='agent': wraps the MCP server as a delegatable agent in the roster.
|
|
540
|
-
* Use only when the remote service has its own reasoning layer or its output
|
|
541
|
-
* is so large it needs a dedicated summarization turn.
|
|
542
|
-
*/
|
|
543
|
-
registerMCPAgentWrapper(config, tools, safeName) {
|
|
544
|
-
const toolList = tools
|
|
545
|
-
.map(t => {
|
|
546
|
-
const shortName = t.name.split('__')[2] ?? t.name;
|
|
547
|
-
const desc = t.description.replace(/^\[MCP:[^\]]+\]\s*/, '').slice(0, 80);
|
|
548
|
-
return `- ${shortName}: ${desc}`;
|
|
549
|
-
})
|
|
550
|
-
.join('\n');
|
|
551
|
-
const enrichedDesc = tools
|
|
552
|
-
.map(t => {
|
|
553
|
-
const shortName = t.name.split('__')[2] ?? t.name;
|
|
554
|
-
const desc = t.description.replace(/^\[MCP:[^\]]+\]\s*/, '').slice(0, 70);
|
|
555
|
-
return `${shortName}: ${desc}`;
|
|
556
|
-
})
|
|
557
|
-
.join(' | ');
|
|
558
|
-
this.opts.agents.upsert({
|
|
559
|
-
id: safeName,
|
|
560
|
-
name: config.agentName ?? toTitleCase(config.name),
|
|
561
|
-
icon: config.icon ?? '◎',
|
|
562
|
-
description: config.agentDescription ?? `External specialist — ${enrichedDesc}`,
|
|
563
|
-
systemPrompt: `You are a specialized remote agent: ${config.name}.\n` +
|
|
564
|
-
`Use ONLY your available tools to answer the request. ` +
|
|
565
|
-
`Do NOT use run_command, read_file, or other local tools.\n\n` +
|
|
566
|
-
`Your tools:\n${toolList}`,
|
|
567
|
-
allowedTools: tools.map(t => t.name),
|
|
568
|
-
source: 'mcp',
|
|
569
|
-
});
|
|
570
|
-
if (config.triggers?.length) {
|
|
571
|
-
this.mcpTriggers.set(safeName, config.triggers);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
findMCPRoute(input) {
|
|
575
|
-
const lower = input.toLowerCase();
|
|
576
|
-
for (const [keyword, route] of this.mcpToolTriggers) {
|
|
577
|
-
if (lower.includes(keyword))
|
|
578
|
-
return route;
|
|
579
|
-
}
|
|
580
|
-
for (const [agentId, triggers] of this.mcpTriggers) {
|
|
581
|
-
if (triggers.some(t => lower.includes(t.toLowerCase())))
|
|
582
|
-
return { agentId };
|
|
583
|
-
}
|
|
584
|
-
return undefined;
|
|
585
|
-
}
|
|
586
361
|
switchAgent(agentId) {
|
|
587
362
|
if (!this.opts.agents.has(agentId))
|
|
588
363
|
throw new Error(`Agent "${agentId}" not found`);
|
|
@@ -697,7 +472,7 @@ class AgentEngine {
|
|
|
697
472
|
if (explicit)
|
|
698
473
|
parts.push(explicit);
|
|
699
474
|
if (trimmed)
|
|
700
|
-
parts.push(`##
|
|
475
|
+
parts.push(`## Orchestrator Analysis\n${trimmed}`);
|
|
701
476
|
return parts.join('\n\n');
|
|
702
477
|
}
|
|
703
478
|
async runLoop(profile, orchestrationResult) {
|
|
@@ -717,7 +492,7 @@ class AgentEngine {
|
|
|
717
492
|
this.trimHistory();
|
|
718
493
|
const systemMsg = { role: 'system', content: this.buildSystemPrompt(currentProfile, orchestrationResult) };
|
|
719
494
|
const tools = this.getToolsForAgent(currentProfile);
|
|
720
|
-
const safeHistory =
|
|
495
|
+
const safeHistory = this.history.sanitized();
|
|
721
496
|
let streamBuffer = '';
|
|
722
497
|
const askAgentCalls = [];
|
|
723
498
|
const otherCalls = [];
|
|
@@ -760,7 +535,7 @@ class AgentEngine {
|
|
|
760
535
|
break;
|
|
761
536
|
if (!finalMessage)
|
|
762
537
|
break;
|
|
763
|
-
this.
|
|
538
|
+
this.history.push(finalMessage);
|
|
764
539
|
const toolResults = [];
|
|
765
540
|
// ── ask_agent: parallel if 2+ calls ──────────────────────────────
|
|
766
541
|
if (askAgentCalls.length > 0) {
|
|
@@ -776,6 +551,7 @@ class AgentEngine {
|
|
|
776
551
|
agentId: c.agentId,
|
|
777
552
|
question: c.question,
|
|
778
553
|
context: this.buildSubAgentContext(c.context, streamBuffer),
|
|
554
|
+
fromAgentId: currentProfile.id,
|
|
779
555
|
}));
|
|
780
556
|
const results = validCalls.length >= 2
|
|
781
557
|
? await this.bus.runParallel(requests)
|
|
@@ -794,7 +570,7 @@ class AgentEngine {
|
|
|
794
570
|
if (this.aborted)
|
|
795
571
|
break;
|
|
796
572
|
iterManager.recordToolCall();
|
|
797
|
-
const result = await this.executeToolCall(call, currentProfile);
|
|
573
|
+
const result = await this.executeToolCall(call, currentProfile, 0);
|
|
798
574
|
toolResults.push({ role: 'tool', tool_call_id: call.id, content: result });
|
|
799
575
|
}
|
|
800
576
|
// ── QA orchestration: runs when response is final (no tool calls) ─
|
|
@@ -806,7 +582,7 @@ class AgentEngine {
|
|
|
806
582
|
orchestrator: this.opts.orchestrator,
|
|
807
583
|
qaService: this.opts.qaService,
|
|
808
584
|
allAgents: this.opts.agents.getAll(),
|
|
809
|
-
messages: this.
|
|
585
|
+
messages: this.history.getAll(),
|
|
810
586
|
effectiveAgentId: currentProfile.id,
|
|
811
587
|
defaultAgentId: this.opts.defaultAgentId,
|
|
812
588
|
qaAgentId,
|
|
@@ -834,11 +610,11 @@ class AgentEngine {
|
|
|
834
610
|
}
|
|
835
611
|
if (toolResults.length === 0)
|
|
836
612
|
break;
|
|
837
|
-
this.
|
|
613
|
+
this.history.pushAll(toolResults);
|
|
838
614
|
}
|
|
839
615
|
this.opts.transport.emit({ type: 'block_end', status: 'ok', agent_id: profile.id });
|
|
840
616
|
}
|
|
841
|
-
async runAgentInternal(agentId, question, context) {
|
|
617
|
+
async runAgentInternal(agentId, question, context, depth = 0, outputBuffer) {
|
|
842
618
|
const profile = this.opts.agents.get(agentId);
|
|
843
619
|
if (!profile)
|
|
844
620
|
throw new Error(`Agent "${agentId}" not registered`);
|
|
@@ -848,28 +624,38 @@ class AgentEngine {
|
|
|
848
624
|
// without requiring the orchestrator to manually copy it into context.
|
|
849
625
|
let historySlice = [];
|
|
850
626
|
const maxCtxMsgs = this.opts.subAgentContextMessages;
|
|
851
|
-
|
|
852
|
-
|
|
627
|
+
const allMessages = this.history.getAll();
|
|
628
|
+
if (maxCtxMsgs && maxCtxMsgs > 0 && allMessages.length > 0) {
|
|
629
|
+
historySlice = allMessages.slice(-maxCtxMsgs);
|
|
853
630
|
// Trim to the first user-turn boundary to avoid orphaning tool results.
|
|
854
631
|
const firstUser = historySlice.findIndex(m => m.role === 'user');
|
|
855
632
|
if (firstUser > 0)
|
|
856
633
|
historySlice = historySlice.slice(firstUser);
|
|
857
634
|
else if (firstUser < 0)
|
|
858
635
|
historySlice = [];
|
|
859
|
-
|
|
860
|
-
historySlice = sanitizeMessages(historySlice);
|
|
636
|
+
historySlice = (0, HistoryManager_1.sanitizeMessages)(historySlice);
|
|
861
637
|
}
|
|
862
638
|
const messages = [
|
|
863
639
|
{ role: 'system', content: this.buildSystemPrompt(profile) },
|
|
864
640
|
...historySlice,
|
|
865
|
-
{ role: 'user', content: context ? `${question}\n\
|
|
641
|
+
{ role: 'user', content: context ? `${question}\n\nContext:\n${context}` : question },
|
|
866
642
|
];
|
|
867
643
|
let finalContent = '';
|
|
868
644
|
let totalInputTokens = 0;
|
|
869
645
|
let totalOutputTokens = 0;
|
|
870
|
-
const maxIter =
|
|
646
|
+
const maxIter = this.opts.maxIterations ?? 100;
|
|
871
647
|
let iter = 0;
|
|
872
|
-
|
|
648
|
+
// When running in parallel (outputBuffer provided), route output events to the
|
|
649
|
+
// buffer instead of the real transport. The caller flushes the buffer atomically
|
|
650
|
+
// after the agent finishes, preventing interleaved output from parallel agents.
|
|
651
|
+
// tool_approval_request always bypasses buffering — it needs an immediate response.
|
|
652
|
+
const emit = (event) => {
|
|
653
|
+
if (outputBuffer)
|
|
654
|
+
outputBuffer.push(event);
|
|
655
|
+
else
|
|
656
|
+
this.opts.transport.emit(event);
|
|
657
|
+
};
|
|
658
|
+
emit({ type: 'block_start', agent_id: profile.id, agent_name: profile.name, agent_icon: profile.icon });
|
|
873
659
|
while (iter < maxIter) {
|
|
874
660
|
iter++;
|
|
875
661
|
let streamContent = '';
|
|
@@ -879,12 +665,12 @@ class AgentEngine {
|
|
|
879
665
|
onChunk: (delta) => {
|
|
880
666
|
streamContent += delta;
|
|
881
667
|
if (!profile.suppressDefaultOutput) {
|
|
882
|
-
|
|
668
|
+
emit({ type: 'token', content: delta, agent_id: profile.id });
|
|
883
669
|
}
|
|
884
670
|
},
|
|
885
671
|
onToolCall: (name, args, id) => {
|
|
886
672
|
if (!profile.suppressDefaultOutput) {
|
|
887
|
-
|
|
673
|
+
emit({ type: 'tool_call', name, args_preview: args.slice(0, 200) });
|
|
888
674
|
}
|
|
889
675
|
toolCalls.push({ id, name, args });
|
|
890
676
|
},
|
|
@@ -893,7 +679,7 @@ class AgentEngine {
|
|
|
893
679
|
onMetrics: (inputTokens, outputTokens) => {
|
|
894
680
|
totalInputTokens += inputTokens;
|
|
895
681
|
totalOutputTokens += outputTokens;
|
|
896
|
-
|
|
682
|
+
emit({ type: 'metrics', input_tokens: inputTokens, output_tokens: outputTokens });
|
|
897
683
|
},
|
|
898
684
|
model: profile.model,
|
|
899
685
|
});
|
|
@@ -906,12 +692,12 @@ class AgentEngine {
|
|
|
906
692
|
break;
|
|
907
693
|
const toolResults = [];
|
|
908
694
|
for (const call of toolCalls) {
|
|
909
|
-
const result = await this.executeToolCall(call, profile);
|
|
695
|
+
const result = await this.executeToolCall(call, profile, depth, emit);
|
|
910
696
|
toolResults.push({ role: 'tool', tool_call_id: call.id, content: result });
|
|
911
697
|
}
|
|
912
698
|
messages.push(...toolResults);
|
|
913
699
|
}
|
|
914
|
-
|
|
700
|
+
emit({ type: 'block_end', status: 'ok', agent_id: profile.id });
|
|
915
701
|
return { agentId, content: finalContent, inputTokens: totalInputTokens, outputTokens: totalOutputTokens, elapsedMs: Date.now() - start };
|
|
916
702
|
}
|
|
917
703
|
/**
|
|
@@ -919,87 +705,47 @@ class AgentEngine {
|
|
|
919
705
|
* runAgentInternal (sub-agents via ask_agent).
|
|
920
706
|
* Handles: allowedTools authorization → onToolApproval gate → execute → audit → transport.
|
|
921
707
|
*/
|
|
922
|
-
async executeToolCall(call, profile) {
|
|
708
|
+
async executeToolCall(call, profile, depth = 0, emit) {
|
|
709
|
+
const emitEvent = emit ?? ((e) => this.opts.transport.emit(e));
|
|
923
710
|
const callStart = Date.now();
|
|
924
711
|
const toolDef = this.opts.tools.get(call.name);
|
|
925
712
|
let result;
|
|
713
|
+
let success;
|
|
926
714
|
if (!toolDef) {
|
|
927
715
|
result = `Tool "${call.name}" not found`;
|
|
716
|
+
success = false;
|
|
928
717
|
}
|
|
929
718
|
else if (!profile.allowedTools.includes(call.name) &&
|
|
930
719
|
(profile.allowedTools.length > 0 || (profile.delegatesTo && profile.delegatesTo.length > 0))) {
|
|
931
720
|
result = `Tool "${call.name}" is not authorized for agent "${profile.id}". ` +
|
|
932
721
|
`Use ask_agent to delegate to a specialist.`;
|
|
933
|
-
|
|
934
|
-
else if (this.opts.onToolApproval && this.requiresAlwaysAsk(call)) {
|
|
935
|
-
// High-blast-radius operations (npm install, pip install, etc.) always
|
|
936
|
-
// require approval — cannot be bypassed by approve_all or auto mode.
|
|
937
|
-
const preview = call.args.slice(0, 200);
|
|
938
|
-
this.opts.transport.emit({
|
|
939
|
-
type: 'tool_approval_request',
|
|
940
|
-
tool_name: call.name,
|
|
941
|
-
agent_id: profile.id,
|
|
942
|
-
args_preview: preview,
|
|
943
|
-
});
|
|
944
|
-
const decision = await this.opts.onToolApproval(call.name, profile.id, preview);
|
|
945
|
-
result = decision === 'reject'
|
|
946
|
-
? `Tool call "${call.name}" was rejected by the user.`
|
|
947
|
-
: await this.runToolDef(call, toolDef, profile.id);
|
|
948
|
-
}
|
|
949
|
-
else if (!this.approvalDisabled &&
|
|
950
|
-
this.opts.onToolApproval &&
|
|
951
|
-
AgentEngine.WRITE_TOOLS.has(call.name)) {
|
|
952
|
-
const preview = call.args.slice(0, 200);
|
|
953
|
-
this.opts.transport.emit({
|
|
954
|
-
type: 'tool_approval_request',
|
|
955
|
-
tool_name: call.name,
|
|
956
|
-
agent_id: profile.id,
|
|
957
|
-
args_preview: preview,
|
|
958
|
-
});
|
|
959
|
-
const decision = await this.opts.onToolApproval(call.name, profile.id, preview);
|
|
960
|
-
if (decision === 'approve_all')
|
|
961
|
-
this.approvalDisabled = true;
|
|
962
|
-
result = decision === 'reject'
|
|
963
|
-
? `Tool call "${call.name}" was rejected by the user.`
|
|
964
|
-
: await this.runToolDef(call, toolDef, profile.id);
|
|
722
|
+
success = false;
|
|
965
723
|
}
|
|
966
724
|
else {
|
|
967
|
-
|
|
725
|
+
const gateDecision = await this.approvalGate.check(call, profile.id);
|
|
726
|
+
if (gateDecision === 'rejected') {
|
|
727
|
+
result = `Tool call "${call.name}" was rejected by the user.`;
|
|
728
|
+
success = false;
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
({ result, success } = await this.runToolDef(call, toolDef, profile.id, depth));
|
|
732
|
+
}
|
|
968
733
|
}
|
|
969
|
-
|
|
970
|
-
this.writeAuditRecord(profile.id, call.name, call.args, success, Date.now() - callStart);
|
|
734
|
+
await this.auditLogger.log({ agentId: profile.id, tool: call.name, inputPreview: call.args, success, durationMs: Date.now() - callStart, sessionId: this.opts.sessionId });
|
|
971
735
|
// Strip ANSI before slicing to avoid partial escape sequences in the preview
|
|
972
736
|
// eslint-disable-next-line no-control-regex
|
|
973
737
|
const cleanPreview = result.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');
|
|
974
|
-
|
|
738
|
+
emitEvent({ type: 'tool_result', name: call.name, success, preview: cleanPreview.slice(0, 120) });
|
|
975
739
|
return result;
|
|
976
740
|
}
|
|
977
|
-
async runToolDef(call, toolDef, agentId) {
|
|
978
|
-
const ctx = this.buildContext(agentId);
|
|
741
|
+
async runToolDef(call, toolDef, agentId, depth = 0) {
|
|
742
|
+
const ctx = this.buildContext(agentId, depth);
|
|
979
743
|
try {
|
|
980
744
|
const args = JSON.parse(call.args);
|
|
981
|
-
return await toolDef.execute(args, ctx);
|
|
745
|
+
return { result: await toolDef.execute(args, ctx), success: true };
|
|
982
746
|
}
|
|
983
747
|
catch (err) {
|
|
984
|
-
return `Error: ${err instanceof Error ? err.message : String(err)}
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
/**
|
|
988
|
-
* Returns true when a tool call matches a high-blast-radius pattern that
|
|
989
|
-
* always requires explicit approval — cannot be bypassed by approve_all or
|
|
990
|
-
* auto iteration mode.
|
|
991
|
-
*/
|
|
992
|
-
requiresAlwaysAsk(call) {
|
|
993
|
-
if (call.name !== 'run_command')
|
|
994
|
-
return false;
|
|
995
|
-
try {
|
|
996
|
-
const { command } = JSON.parse(call.args);
|
|
997
|
-
if (!command)
|
|
998
|
-
return false;
|
|
999
|
-
return AgentEngine.ALWAYS_ASK_PATTERNS.some(re => re.test(command));
|
|
1000
|
-
}
|
|
1001
|
-
catch {
|
|
1002
|
-
return false;
|
|
748
|
+
return { result: `Error: ${err instanceof Error ? err.message : String(err)}`, success: false };
|
|
1003
749
|
}
|
|
1004
750
|
}
|
|
1005
751
|
/**
|
|
@@ -1012,56 +758,13 @@ class AgentEngine {
|
|
|
1012
758
|
* For mode='tools' servers, agents fall back to shell commands automatically.
|
|
1013
759
|
*/
|
|
1014
760
|
unregisterMCPServer(serverName) {
|
|
1015
|
-
|
|
1016
|
-
// Revert tool assignments and command blocks on affected agents
|
|
1017
|
-
const records = this.mcpAgentBlocks.get(safeName) ?? [];
|
|
1018
|
-
for (const { agentId, toolNames, blockedCmds } of records) {
|
|
1019
|
-
const agent = this.opts.agents.get(agentId);
|
|
1020
|
-
if (agent) {
|
|
1021
|
-
this.opts.agents.upsert({
|
|
1022
|
-
...agent,
|
|
1023
|
-
allowedTools: agent.allowedTools.filter(t => !toolNames.includes(t)),
|
|
1024
|
-
blockedCommands: (agent.blockedCommands ?? []).filter(c => !blockedCmds.includes(c)),
|
|
1025
|
-
});
|
|
1026
|
-
}
|
|
1027
|
-
for (const toolName of toolNames) {
|
|
1028
|
-
this.opts.tools.unregister(toolName);
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
this.mcpAgentBlocks.delete(safeName);
|
|
1032
|
-
// Remove wrapper agent if mode='agent'
|
|
1033
|
-
this.opts.agents.delete(safeName);
|
|
1034
|
-
// Clean up routing tables
|
|
1035
|
-
this.mcpTriggers.delete(safeName);
|
|
1036
|
-
for (const [keyword, route] of this.mcpToolTriggers) {
|
|
1037
|
-
if (route.agentId === safeName)
|
|
1038
|
-
this.mcpToolTriggers.delete(keyword);
|
|
1039
|
-
}
|
|
1040
|
-
this.mcpRegisteredServers.delete(serverName);
|
|
1041
|
-
this.opts.transport.emit({
|
|
1042
|
-
type: 'system', level: 'info',
|
|
1043
|
-
text: `[MCP:${serverName}] unregistered — tools removed, commands unblocked.`,
|
|
1044
|
-
});
|
|
761
|
+
this.mcpRegistrar.unregisterServer(serverName);
|
|
1045
762
|
}
|
|
1046
763
|
/** Disable approval gate for the rest of this session (approve_all). */
|
|
1047
|
-
disableToolApproval() { this.
|
|
764
|
+
disableToolApproval() { this.approvalGate.disable(); }
|
|
1048
765
|
/** Re-enable approval gate (e.g. after /iteration manual). */
|
|
1049
|
-
enableToolApproval() { this.
|
|
1050
|
-
|
|
1051
|
-
if (!this.opts.auditLog)
|
|
1052
|
-
return;
|
|
1053
|
-
const line = JSON.stringify({
|
|
1054
|
-
ts: new Date().toISOString(),
|
|
1055
|
-
session: this.opts.sessionId ?? 'default',
|
|
1056
|
-
agent: agentId,
|
|
1057
|
-
tool,
|
|
1058
|
-
input: inputPreview.slice(0, 200),
|
|
1059
|
-
success,
|
|
1060
|
-
durationMs,
|
|
1061
|
-
});
|
|
1062
|
-
(0, fs_1.appendFileSync)(this.opts.auditLog, line + '\n');
|
|
1063
|
-
}
|
|
1064
|
-
buildContext(agentId) {
|
|
766
|
+
enableToolApproval() { this.approvalGate.enable(); }
|
|
767
|
+
buildContext(agentId, depth = 0) {
|
|
1065
768
|
const id = agentId ?? this.currentAgentId;
|
|
1066
769
|
const profile = this.opts.agents.get(id);
|
|
1067
770
|
const storeRoot = this.opts.store?.projectDir;
|
|
@@ -1079,14 +782,14 @@ class AgentEngine {
|
|
|
1079
782
|
...(additionalRoots.length ? { additionalRoots } : {}),
|
|
1080
783
|
...(profile?.blockedCommands?.length ? { blockedCommands: profile.blockedCommands } : {}),
|
|
1081
784
|
askAgent: async (agentId, question, context) => {
|
|
1082
|
-
const result = await this.bus.runSingle({ agentId, question, context });
|
|
785
|
+
const result = await this.bus.runSingle({ agentId, question, context, fromAgentId: id, depth: depth + 1 });
|
|
1083
786
|
return result.content;
|
|
1084
787
|
},
|
|
1085
788
|
emitCompacting: (phase, label) => {
|
|
1086
789
|
this.opts.transport.emit({ type: 'compacting', phase, label });
|
|
1087
790
|
},
|
|
1088
791
|
llmCall: async (prompt) => {
|
|
1089
|
-
const profile = this.opts.agents.get(
|
|
792
|
+
const profile = this.opts.agents.get(id);
|
|
1090
793
|
if (!profile)
|
|
1091
794
|
return '';
|
|
1092
795
|
const provider = this.getProviderForProfile(profile);
|
|
@@ -1103,9 +806,4 @@ class AgentEngine {
|
|
|
1103
806
|
}
|
|
1104
807
|
}
|
|
1105
808
|
exports.AgentEngine = AgentEngine;
|
|
1106
|
-
function toTitleCase(s) {
|
|
1107
|
-
return s
|
|
1108
|
-
.replace(/[-_](.)/g, (_, c) => ' ' + c.toUpperCase())
|
|
1109
|
-
.replace(/^(.)/, (c) => c.toUpperCase());
|
|
1110
|
-
}
|
|
1111
809
|
//# sourceMappingURL=AgentEngine.js.map
|