@backendkit-labs/agent-core 0.20.2 → 0.22.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/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 +125 -431
- 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 +45 -0
- package/dist/engine/MCPRegistrar.d.ts.map +1 -0
- package/dist/engine/MCPRegistrar.js +217 -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 +15 -0
- package/dist/mcp/MCPClientManager.d.ts.map +1 -1
- package/dist/mcp/MCPClientManager.js +62 -8
- 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.
|
|
@@ -249,6 +190,21 @@ class AgentEngine {
|
|
|
249
190
|
return this.opts.providers.resolve(profile.provider, this.opts.defaultProvider);
|
|
250
191
|
}
|
|
251
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) {
|
|
252
208
|
if (this.pendingContextLoad)
|
|
253
209
|
await this.pendingContextLoad;
|
|
254
210
|
this.aborted = false;
|
|
@@ -262,27 +218,27 @@ class AgentEngine {
|
|
|
262
218
|
this.opts.transport.emit({ type: 'done' });
|
|
263
219
|
return;
|
|
264
220
|
}
|
|
265
|
-
await this.
|
|
221
|
+
await this.mcpRegistrar.ensureInitialized();
|
|
266
222
|
await this.ensureAgentsLoaded();
|
|
267
223
|
// Direct MCP routing: bypass general when input matches a configured trigger
|
|
268
|
-
const mcpRoute = this.
|
|
224
|
+
const mcpRoute = this.mcpRegistrar.findRoute(input);
|
|
269
225
|
if (mcpRoute) {
|
|
270
226
|
if (mcpRoute.tool) {
|
|
271
|
-
// 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.
|
|
272
229
|
const toolName = `mcp__${mcpRoute.agentId}__${mcpRoute.tool}`;
|
|
273
230
|
const toolDef = this.opts.tools.get(toolName);
|
|
274
231
|
if (toolDef) {
|
|
275
232
|
const required = toolDef.parameters.required;
|
|
276
233
|
const firstParam = required?.[0] ?? 'input';
|
|
277
|
-
const
|
|
278
|
-
const profile = this.opts.agents.get(mcpRoute.agentId);
|
|
234
|
+
const mcpProfile = this.opts.agents.get(mcpRoute.agentId) ?? this.getCurrentProfile();
|
|
279
235
|
this.opts.transport.emit({
|
|
280
236
|
type: 'block_start',
|
|
281
237
|
agent_id: mcpRoute.agentId,
|
|
282
|
-
agent_name:
|
|
283
|
-
agent_icon:
|
|
238
|
+
agent_name: mcpProfile.name ?? mcpRoute.agentId,
|
|
239
|
+
agent_icon: mcpProfile.icon ?? '◎',
|
|
284
240
|
});
|
|
285
|
-
const result = await
|
|
241
|
+
const result = await this.executeToolCall({ name: toolName, args: JSON.stringify({ [firstParam]: input }) }, mcpProfile);
|
|
286
242
|
this.opts.transport.emit({ type: 'token', content: result, agent_id: mcpRoute.agentId });
|
|
287
243
|
this.opts.transport.emit({ type: 'block_end', status: 'ok', agent_id: mcpRoute.agentId });
|
|
288
244
|
this.opts.transport.emit({ type: 'done' });
|
|
@@ -320,7 +276,7 @@ class AgentEngine {
|
|
|
320
276
|
if (this.opts.orchestrator) {
|
|
321
277
|
orchestrationResult = await this.opts.orchestrator.orchestrate(input).catch(() => undefined);
|
|
322
278
|
}
|
|
323
|
-
this.
|
|
279
|
+
this.history.push({ role: 'user', content: input });
|
|
324
280
|
await this.runLoop(profile, orchestrationResult);
|
|
325
281
|
this.saveHistory();
|
|
326
282
|
this.opts.transport.emit({ type: 'done' });
|
|
@@ -328,12 +284,12 @@ class AgentEngine {
|
|
|
328
284
|
async runWorkflow(defOrBuilder, opts = {}) {
|
|
329
285
|
if (this.pendingContextLoad)
|
|
330
286
|
await this.pendingContextLoad;
|
|
331
|
-
await this.
|
|
287
|
+
await this.mcpRegistrar.ensureInitialized();
|
|
332
288
|
await this.ensureAgentsLoaded();
|
|
333
289
|
const def = 'build' in defOrBuilder ? defOrBuilder.build() : defOrBuilder;
|
|
334
290
|
const runner = new WorkflowRunner_1.WorkflowRunner(def, {
|
|
335
291
|
...opts,
|
|
336
|
-
runAgent: (agentId, input, context) => this.runAgentInternal(agentId, input, context),
|
|
292
|
+
runAgent: (agentId, input, context) => this.runAgentInternal(agentId, input, context, 0),
|
|
337
293
|
transport: this.opts.transport,
|
|
338
294
|
});
|
|
339
295
|
return runner.run();
|
|
@@ -353,59 +309,39 @@ class AgentEngine {
|
|
|
353
309
|
}
|
|
354
310
|
/** Delete persisted history file and reset in-memory conversation. */
|
|
355
311
|
clearHistory() {
|
|
356
|
-
this.
|
|
357
|
-
if (!this.opts.historyPath)
|
|
358
|
-
return;
|
|
359
|
-
try {
|
|
360
|
-
(0, atomic_write_1.atomicWriteSync)(this.opts.historyPath, '[]');
|
|
361
|
-
}
|
|
362
|
-
catch { /* non-fatal */ }
|
|
312
|
+
this.history.clear();
|
|
363
313
|
}
|
|
364
|
-
/** Serialize current conversation to disk (called automatically after each run). */
|
|
365
314
|
saveHistory() {
|
|
366
|
-
|
|
367
|
-
return;
|
|
368
|
-
const cap = this.opts.maxContextMessages ?? 100;
|
|
369
|
-
const toSave = this.messages.filter(m => m.role !== 'system').slice(-cap);
|
|
370
|
-
try {
|
|
371
|
-
(0, fs_1.mkdirSync)((0, path_1.dirname)(this.opts.historyPath), { recursive: true });
|
|
372
|
-
(0, atomic_write_1.atomicWriteSync)(this.opts.historyPath, JSON.stringify(toSave, null, 2));
|
|
373
|
-
}
|
|
374
|
-
catch { /* non-fatal — history loss is preferable to a crash */ }
|
|
315
|
+
this.history.save();
|
|
375
316
|
}
|
|
376
|
-
/**
|
|
377
|
-
* Trim conversation history to maxContextMessages by dropping oldest messages
|
|
378
|
-
* at a user-turn boundary. Preserves all assistant↔tool pairs intact.
|
|
379
|
-
*/
|
|
380
317
|
trimHistory() {
|
|
381
318
|
const max = this.opts.maxContextMessages;
|
|
382
|
-
if (!max
|
|
319
|
+
if (!max)
|
|
383
320
|
return;
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const target = this.messages.length - max;
|
|
387
|
-
let cutAt = target;
|
|
388
|
-
while (cutAt < this.messages.length && this.messages[cutAt].role !== 'user') {
|
|
389
|
-
cutAt++;
|
|
390
|
-
}
|
|
391
|
-
if (cutAt > 0 && cutAt < this.messages.length) {
|
|
392
|
-
this.messages = this.messages.slice(cutAt);
|
|
321
|
+
const dropped = this.history.trim(max);
|
|
322
|
+
if (dropped > 0) {
|
|
393
323
|
this.opts.transport.emit({
|
|
394
324
|
type: 'system',
|
|
395
325
|
level: 'warn',
|
|
396
|
-
text: `[ContextWindow] Dropped ${
|
|
326
|
+
text: `[ContextWindow] Dropped ${dropped} messages — history capped at ${max} (maxContextMessages)`,
|
|
397
327
|
});
|
|
398
328
|
}
|
|
399
329
|
}
|
|
400
330
|
async ensureAgentsLoaded() {
|
|
401
331
|
if (this.agentsLoaded || !this.opts.loadAgents)
|
|
402
332
|
return;
|
|
403
|
-
|
|
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() {
|
|
404
341
|
try {
|
|
405
342
|
const discovered = await this.opts.loadAgents();
|
|
406
343
|
for (const agent of discovered)
|
|
407
344
|
this.opts.agents.upsert(agent);
|
|
408
|
-
// If the current default agent still isn't registered, fall back to first available
|
|
409
345
|
if (!this.opts.agents.has(this.currentAgentId)) {
|
|
410
346
|
const all = this.opts.agents.getAll();
|
|
411
347
|
if (all.length > 0)
|
|
@@ -418,175 +354,10 @@ class AgentEngine {
|
|
|
418
354
|
text: `[AgentDiscovery] Failed to load agents: ${err.message}`,
|
|
419
355
|
});
|
|
420
356
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
if (!this.opts.mcpManager)
|
|
424
|
-
return;
|
|
425
|
-
try {
|
|
426
|
-
if (!this.mcpInitialized) {
|
|
427
|
-
await this.opts.mcpManager.initialize();
|
|
428
|
-
this.mcpInitialized = true;
|
|
429
|
-
}
|
|
430
|
-
else {
|
|
431
|
-
// Subsequent runs: retry only servers that are still offline — bounded by their own counter
|
|
432
|
-
const offline = this.opts.mcpManager.getConfiguredNames()
|
|
433
|
-
.filter(n => !this.mcpRegisteredServers.has(n));
|
|
434
|
-
if (offline.length > 0 && this.mcpInitAttempts < 3) {
|
|
435
|
-
this.mcpInitAttempts++;
|
|
436
|
-
await this.opts.mcpManager.reconnectFailed();
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
catch (err) {
|
|
441
|
-
this.opts.transport.emit({
|
|
442
|
-
type: 'system', level: 'warn',
|
|
443
|
-
text: `MCP initialization error: ${err.message}`,
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
// Always register any server that is now connected but not yet registered as agent.
|
|
447
|
-
// This runs regardless of the retry counter so late-starting servers get picked up.
|
|
448
|
-
for (const { name, toolCount } of this.opts.mcpManager.getServerInfo()) {
|
|
449
|
-
if (toolCount > 0 && !this.mcpRegisteredServers.has(name)) {
|
|
450
|
-
const tools = this.opts.mcpManager.getServerTools(name);
|
|
451
|
-
const config = this.opts.mcpManager.getConfig(name);
|
|
452
|
-
this.registerMCPAgent(config, tools);
|
|
453
|
-
this.mcpRegisteredServers.add(name);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
// Warn about servers still offline (only while retries remain)
|
|
457
|
-
const stillOffline = this.opts.mcpManager.getConfiguredNames()
|
|
458
|
-
.filter(n => !this.mcpRegisteredServers.has(n));
|
|
459
|
-
if (stillOffline.length > 0) {
|
|
460
|
-
const retriesLeft = 3 - this.mcpInitAttempts;
|
|
461
|
-
const suffix = retriesLeft > 0
|
|
462
|
-
? `Retrying on next message (${retriesLeft} attempts left).`
|
|
463
|
-
: `No more automatic retries — call engine.getMCPManager()?.reconnectFailed() to retry manually.`;
|
|
464
|
-
this.opts.transport.emit({
|
|
465
|
-
type: 'system', level: 'warn',
|
|
466
|
-
text: `MCP agents offline: ${stillOffline.join(', ')} — start their servers to enable them. ${suffix}`,
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
registerMCPAgent(config, tools) {
|
|
471
|
-
// safeName mirrors the sanitization done by MCPClientManager.wrapTool() so that
|
|
472
|
-
// tool lookup `mcp__${safeName}__${tool}` always resolves correctly.
|
|
473
|
-
const safeName = config.name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
474
|
-
// Register tools in the ToolRegistry — skip already-registered ones
|
|
475
|
-
// (safe for reconnection scenarios; mcpRegisteredServers normally prevents duplicates)
|
|
476
|
-
for (const tool of tools) {
|
|
477
|
-
if (!this.opts.tools.has(tool.name)) {
|
|
478
|
-
this.opts.tools.register(tool);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
// toolTriggers work in both modes — direct keyword → tool execution, zero LLM hops
|
|
482
|
-
if (config.toolTriggers) {
|
|
483
|
-
for (const [keyword, tool] of Object.entries(config.toolTriggers)) {
|
|
484
|
-
this.mcpToolTriggers.set(keyword.toLowerCase(), { agentId: safeName, tool });
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
const mode = config.mode ?? 'tools';
|
|
488
|
-
if (mode === 'tools') {
|
|
489
|
-
this.registerMCPToolPack(config, tools, safeName);
|
|
490
|
-
}
|
|
491
|
-
else {
|
|
492
|
-
this.registerMCPAgentWrapper(config, tools, safeName);
|
|
357
|
+
finally {
|
|
358
|
+
this.agentsLoaded = true;
|
|
493
359
|
}
|
|
494
360
|
}
|
|
495
|
-
/**
|
|
496
|
-
* mode='tools' (default): assigns MCP tools directly to local agent allowedTools.
|
|
497
|
-
* Also applies blocksCommands to those agents while the server is connected —
|
|
498
|
-
* forcing them to use the MCP tools instead of equivalent shell commands.
|
|
499
|
-
* Both tool assignments and command blocks are tracked so they can be
|
|
500
|
-
* fully reverted when the server disconnects via unregisterMCPServer().
|
|
501
|
-
*/
|
|
502
|
-
registerMCPToolPack(config, tools, safeName) {
|
|
503
|
-
const toolNames = tools.map(t => t.name);
|
|
504
|
-
const blocksCommands = config.blocksCommands ?? [];
|
|
505
|
-
// Resolve target agents: explicit assignTo list, or all registered agents
|
|
506
|
-
const assignTo = config.assignTo?.filter(id => id !== '*') ?? [];
|
|
507
|
-
const targets = assignTo.length > 0
|
|
508
|
-
? assignTo
|
|
509
|
-
: this.opts.agents.getAll().map(a => a.id);
|
|
510
|
-
const blockRecord = [];
|
|
511
|
-
for (const agentId of targets) {
|
|
512
|
-
const agent = this.opts.agents.get(agentId);
|
|
513
|
-
if (!agent) {
|
|
514
|
-
this.opts.transport.emit({
|
|
515
|
-
type: 'system', level: 'warn',
|
|
516
|
-
text: `[MCP:${config.name}] assignTo agent "${agentId}" not found — skipped.`,
|
|
517
|
-
});
|
|
518
|
-
continue;
|
|
519
|
-
}
|
|
520
|
-
this.opts.agents.upsert({
|
|
521
|
-
...agent,
|
|
522
|
-
allowedTools: [...new Set([...agent.allowedTools, ...toolNames])],
|
|
523
|
-
blockedCommands: [...new Set([...(agent.blockedCommands ?? []), ...blocksCommands])],
|
|
524
|
-
});
|
|
525
|
-
blockRecord.push({ agentId, toolNames, blockedCmds: blocksCommands });
|
|
526
|
-
}
|
|
527
|
-
this.mcpAgentBlocks.set(safeName, blockRecord);
|
|
528
|
-
if (config.triggers?.length) {
|
|
529
|
-
this.opts.transport.emit({
|
|
530
|
-
type: 'system', level: 'warn',
|
|
531
|
-
text: `[MCP:${config.name}] 'triggers' is ignored in mode='tools'. Use 'toolTriggers' for direct keyword routing.`,
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
const shortNames = tools.map(t => t.name.split('__')[2] ?? t.name).join(', ');
|
|
535
|
-
const agentList = targets.join(', ') || 'all agents';
|
|
536
|
-
const blockedLabel = blocksCommands.length ? ` blocked: [${blocksCommands.join(', ')}]` : '';
|
|
537
|
-
this.opts.transport.emit({
|
|
538
|
-
type: 'system', level: 'info',
|
|
539
|
-
text: `[MCP:${config.name}] tools [${shortNames}] assigned to: ${agentList}${blockedLabel}`,
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
/**
|
|
543
|
-
* mode='agent': wraps the MCP server as a delegatable agent in the roster.
|
|
544
|
-
* Use only when the remote service has its own reasoning layer or its output
|
|
545
|
-
* is so large it needs a dedicated summarization turn.
|
|
546
|
-
*/
|
|
547
|
-
registerMCPAgentWrapper(config, tools, safeName) {
|
|
548
|
-
const toolList = tools
|
|
549
|
-
.map(t => {
|
|
550
|
-
const shortName = t.name.split('__')[2] ?? t.name;
|
|
551
|
-
const desc = t.description.replace(/^\[MCP:[^\]]+\]\s*/, '').slice(0, 80);
|
|
552
|
-
return `- ${shortName}: ${desc}`;
|
|
553
|
-
})
|
|
554
|
-
.join('\n');
|
|
555
|
-
const enrichedDesc = tools
|
|
556
|
-
.map(t => {
|
|
557
|
-
const shortName = t.name.split('__')[2] ?? t.name;
|
|
558
|
-
const desc = t.description.replace(/^\[MCP:[^\]]+\]\s*/, '').slice(0, 70);
|
|
559
|
-
return `${shortName}: ${desc}`;
|
|
560
|
-
})
|
|
561
|
-
.join(' | ');
|
|
562
|
-
this.opts.agents.upsert({
|
|
563
|
-
id: safeName,
|
|
564
|
-
name: config.agentName ?? toTitleCase(config.name),
|
|
565
|
-
icon: config.icon ?? '◎',
|
|
566
|
-
description: config.agentDescription ?? `External specialist — ${enrichedDesc}`,
|
|
567
|
-
systemPrompt: `You are a specialized remote agent: ${config.name}.\n` +
|
|
568
|
-
`Use ONLY your available tools to answer the request. ` +
|
|
569
|
-
`Do NOT use run_command, read_file, or other local tools.\n\n` +
|
|
570
|
-
`Your tools:\n${toolList}`,
|
|
571
|
-
allowedTools: tools.map(t => t.name),
|
|
572
|
-
source: 'mcp',
|
|
573
|
-
});
|
|
574
|
-
if (config.triggers?.length) {
|
|
575
|
-
this.mcpTriggers.set(safeName, config.triggers);
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
findMCPRoute(input) {
|
|
579
|
-
const lower = input.toLowerCase();
|
|
580
|
-
for (const [keyword, route] of this.mcpToolTriggers) {
|
|
581
|
-
if (lower.includes(keyword))
|
|
582
|
-
return route;
|
|
583
|
-
}
|
|
584
|
-
for (const [agentId, triggers] of this.mcpTriggers) {
|
|
585
|
-
if (triggers.some(t => lower.includes(t.toLowerCase())))
|
|
586
|
-
return { agentId };
|
|
587
|
-
}
|
|
588
|
-
return undefined;
|
|
589
|
-
}
|
|
590
361
|
switchAgent(agentId) {
|
|
591
362
|
if (!this.opts.agents.has(agentId))
|
|
592
363
|
throw new Error(`Agent "${agentId}" not found`);
|
|
@@ -701,7 +472,7 @@ class AgentEngine {
|
|
|
701
472
|
if (explicit)
|
|
702
473
|
parts.push(explicit);
|
|
703
474
|
if (trimmed)
|
|
704
|
-
parts.push(`##
|
|
475
|
+
parts.push(`## Orchestrator Analysis\n${trimmed}`);
|
|
705
476
|
return parts.join('\n\n');
|
|
706
477
|
}
|
|
707
478
|
async runLoop(profile, orchestrationResult) {
|
|
@@ -721,7 +492,7 @@ class AgentEngine {
|
|
|
721
492
|
this.trimHistory();
|
|
722
493
|
const systemMsg = { role: 'system', content: this.buildSystemPrompt(currentProfile, orchestrationResult) };
|
|
723
494
|
const tools = this.getToolsForAgent(currentProfile);
|
|
724
|
-
const safeHistory =
|
|
495
|
+
const safeHistory = this.history.sanitized();
|
|
725
496
|
let streamBuffer = '';
|
|
726
497
|
const askAgentCalls = [];
|
|
727
498
|
const otherCalls = [];
|
|
@@ -764,7 +535,7 @@ class AgentEngine {
|
|
|
764
535
|
break;
|
|
765
536
|
if (!finalMessage)
|
|
766
537
|
break;
|
|
767
|
-
this.
|
|
538
|
+
this.history.push(finalMessage);
|
|
768
539
|
const toolResults = [];
|
|
769
540
|
// ── ask_agent: parallel if 2+ calls ──────────────────────────────
|
|
770
541
|
if (askAgentCalls.length > 0) {
|
|
@@ -780,6 +551,7 @@ class AgentEngine {
|
|
|
780
551
|
agentId: c.agentId,
|
|
781
552
|
question: c.question,
|
|
782
553
|
context: this.buildSubAgentContext(c.context, streamBuffer),
|
|
554
|
+
fromAgentId: currentProfile.id,
|
|
783
555
|
}));
|
|
784
556
|
const results = validCalls.length >= 2
|
|
785
557
|
? await this.bus.runParallel(requests)
|
|
@@ -798,7 +570,7 @@ class AgentEngine {
|
|
|
798
570
|
if (this.aborted)
|
|
799
571
|
break;
|
|
800
572
|
iterManager.recordToolCall();
|
|
801
|
-
const result = await this.executeToolCall(call, currentProfile);
|
|
573
|
+
const result = await this.executeToolCall(call, currentProfile, 0);
|
|
802
574
|
toolResults.push({ role: 'tool', tool_call_id: call.id, content: result });
|
|
803
575
|
}
|
|
804
576
|
// ── QA orchestration: runs when response is final (no tool calls) ─
|
|
@@ -810,7 +582,7 @@ class AgentEngine {
|
|
|
810
582
|
orchestrator: this.opts.orchestrator,
|
|
811
583
|
qaService: this.opts.qaService,
|
|
812
584
|
allAgents: this.opts.agents.getAll(),
|
|
813
|
-
messages: this.
|
|
585
|
+
messages: this.history.getAll(),
|
|
814
586
|
effectiveAgentId: currentProfile.id,
|
|
815
587
|
defaultAgentId: this.opts.defaultAgentId,
|
|
816
588
|
qaAgentId,
|
|
@@ -838,11 +610,11 @@ class AgentEngine {
|
|
|
838
610
|
}
|
|
839
611
|
if (toolResults.length === 0)
|
|
840
612
|
break;
|
|
841
|
-
this.
|
|
613
|
+
this.history.pushAll(toolResults);
|
|
842
614
|
}
|
|
843
615
|
this.opts.transport.emit({ type: 'block_end', status: 'ok', agent_id: profile.id });
|
|
844
616
|
}
|
|
845
|
-
async runAgentInternal(agentId, question, context) {
|
|
617
|
+
async runAgentInternal(agentId, question, context, depth = 0, outputBuffer) {
|
|
846
618
|
const profile = this.opts.agents.get(agentId);
|
|
847
619
|
if (!profile)
|
|
848
620
|
throw new Error(`Agent "${agentId}" not registered`);
|
|
@@ -852,28 +624,38 @@ class AgentEngine {
|
|
|
852
624
|
// without requiring the orchestrator to manually copy it into context.
|
|
853
625
|
let historySlice = [];
|
|
854
626
|
const maxCtxMsgs = this.opts.subAgentContextMessages;
|
|
855
|
-
|
|
856
|
-
|
|
627
|
+
const allMessages = this.history.getAll();
|
|
628
|
+
if (maxCtxMsgs && maxCtxMsgs > 0 && allMessages.length > 0) {
|
|
629
|
+
historySlice = allMessages.slice(-maxCtxMsgs);
|
|
857
630
|
// Trim to the first user-turn boundary to avoid orphaning tool results.
|
|
858
631
|
const firstUser = historySlice.findIndex(m => m.role === 'user');
|
|
859
632
|
if (firstUser > 0)
|
|
860
633
|
historySlice = historySlice.slice(firstUser);
|
|
861
634
|
else if (firstUser < 0)
|
|
862
635
|
historySlice = [];
|
|
863
|
-
|
|
864
|
-
historySlice = sanitizeMessages(historySlice);
|
|
636
|
+
historySlice = (0, HistoryManager_1.sanitizeMessages)(historySlice);
|
|
865
637
|
}
|
|
866
638
|
const messages = [
|
|
867
639
|
{ role: 'system', content: this.buildSystemPrompt(profile) },
|
|
868
640
|
...historySlice,
|
|
869
|
-
{ role: 'user', content: context ? `${question}\n\
|
|
641
|
+
{ role: 'user', content: context ? `${question}\n\nContext:\n${context}` : question },
|
|
870
642
|
];
|
|
871
643
|
let finalContent = '';
|
|
872
644
|
let totalInputTokens = 0;
|
|
873
645
|
let totalOutputTokens = 0;
|
|
874
|
-
const maxIter =
|
|
646
|
+
const maxIter = this.opts.maxIterations ?? 100;
|
|
875
647
|
let iter = 0;
|
|
876
|
-
|
|
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 });
|
|
877
659
|
while (iter < maxIter) {
|
|
878
660
|
iter++;
|
|
879
661
|
let streamContent = '';
|
|
@@ -883,12 +665,12 @@ class AgentEngine {
|
|
|
883
665
|
onChunk: (delta) => {
|
|
884
666
|
streamContent += delta;
|
|
885
667
|
if (!profile.suppressDefaultOutput) {
|
|
886
|
-
|
|
668
|
+
emit({ type: 'token', content: delta, agent_id: profile.id });
|
|
887
669
|
}
|
|
888
670
|
},
|
|
889
671
|
onToolCall: (name, args, id) => {
|
|
890
672
|
if (!profile.suppressDefaultOutput) {
|
|
891
|
-
|
|
673
|
+
emit({ type: 'tool_call', name, args_preview: args.slice(0, 200) });
|
|
892
674
|
}
|
|
893
675
|
toolCalls.push({ id, name, args });
|
|
894
676
|
},
|
|
@@ -897,7 +679,7 @@ class AgentEngine {
|
|
|
897
679
|
onMetrics: (inputTokens, outputTokens) => {
|
|
898
680
|
totalInputTokens += inputTokens;
|
|
899
681
|
totalOutputTokens += outputTokens;
|
|
900
|
-
|
|
682
|
+
emit({ type: 'metrics', input_tokens: inputTokens, output_tokens: outputTokens });
|
|
901
683
|
},
|
|
902
684
|
model: profile.model,
|
|
903
685
|
});
|
|
@@ -910,12 +692,12 @@ class AgentEngine {
|
|
|
910
692
|
break;
|
|
911
693
|
const toolResults = [];
|
|
912
694
|
for (const call of toolCalls) {
|
|
913
|
-
const result = await this.executeToolCall(call, profile);
|
|
695
|
+
const result = await this.executeToolCall(call, profile, depth, emit);
|
|
914
696
|
toolResults.push({ role: 'tool', tool_call_id: call.id, content: result });
|
|
915
697
|
}
|
|
916
698
|
messages.push(...toolResults);
|
|
917
699
|
}
|
|
918
|
-
|
|
700
|
+
emit({ type: 'block_end', status: 'ok', agent_id: profile.id });
|
|
919
701
|
return { agentId, content: finalContent, inputTokens: totalInputTokens, outputTokens: totalOutputTokens, elapsedMs: Date.now() - start };
|
|
920
702
|
}
|
|
921
703
|
/**
|
|
@@ -923,87 +705,47 @@ class AgentEngine {
|
|
|
923
705
|
* runAgentInternal (sub-agents via ask_agent).
|
|
924
706
|
* Handles: allowedTools authorization → onToolApproval gate → execute → audit → transport.
|
|
925
707
|
*/
|
|
926
|
-
async executeToolCall(call, profile) {
|
|
708
|
+
async executeToolCall(call, profile, depth = 0, emit) {
|
|
709
|
+
const emitEvent = emit ?? ((e) => this.opts.transport.emit(e));
|
|
927
710
|
const callStart = Date.now();
|
|
928
711
|
const toolDef = this.opts.tools.get(call.name);
|
|
929
712
|
let result;
|
|
713
|
+
let success;
|
|
930
714
|
if (!toolDef) {
|
|
931
715
|
result = `Tool "${call.name}" not found`;
|
|
716
|
+
success = false;
|
|
932
717
|
}
|
|
933
718
|
else if (!profile.allowedTools.includes(call.name) &&
|
|
934
719
|
(profile.allowedTools.length > 0 || (profile.delegatesTo && profile.delegatesTo.length > 0))) {
|
|
935
720
|
result = `Tool "${call.name}" is not authorized for agent "${profile.id}". ` +
|
|
936
721
|
`Use ask_agent to delegate to a specialist.`;
|
|
937
|
-
|
|
938
|
-
else if (this.opts.onToolApproval && this.requiresAlwaysAsk(call)) {
|
|
939
|
-
// High-blast-radius operations (npm install, pip install, etc.) always
|
|
940
|
-
// require approval — cannot be bypassed by approve_all or auto mode.
|
|
941
|
-
const preview = call.args.slice(0, 200);
|
|
942
|
-
this.opts.transport.emit({
|
|
943
|
-
type: 'tool_approval_request',
|
|
944
|
-
tool_name: call.name,
|
|
945
|
-
agent_id: profile.id,
|
|
946
|
-
args_preview: preview,
|
|
947
|
-
});
|
|
948
|
-
const decision = await this.opts.onToolApproval(call.name, profile.id, preview);
|
|
949
|
-
result = decision === 'reject'
|
|
950
|
-
? `Tool call "${call.name}" was rejected by the user.`
|
|
951
|
-
: await this.runToolDef(call, toolDef, profile.id);
|
|
952
|
-
}
|
|
953
|
-
else if (!this.approvalDisabled &&
|
|
954
|
-
this.opts.onToolApproval &&
|
|
955
|
-
AgentEngine.WRITE_TOOLS.has(call.name)) {
|
|
956
|
-
const preview = call.args.slice(0, 200);
|
|
957
|
-
this.opts.transport.emit({
|
|
958
|
-
type: 'tool_approval_request',
|
|
959
|
-
tool_name: call.name,
|
|
960
|
-
agent_id: profile.id,
|
|
961
|
-
args_preview: preview,
|
|
962
|
-
});
|
|
963
|
-
const decision = await this.opts.onToolApproval(call.name, profile.id, preview);
|
|
964
|
-
if (decision === 'approve_all')
|
|
965
|
-
this.approvalDisabled = true;
|
|
966
|
-
result = decision === 'reject'
|
|
967
|
-
? `Tool call "${call.name}" was rejected by the user.`
|
|
968
|
-
: await this.runToolDef(call, toolDef, profile.id);
|
|
722
|
+
success = false;
|
|
969
723
|
}
|
|
970
724
|
else {
|
|
971
|
-
|
|
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
|
+
}
|
|
972
733
|
}
|
|
973
|
-
|
|
974
|
-
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 });
|
|
975
735
|
// Strip ANSI before slicing to avoid partial escape sequences in the preview
|
|
976
736
|
// eslint-disable-next-line no-control-regex
|
|
977
737
|
const cleanPreview = result.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '');
|
|
978
|
-
|
|
738
|
+
emitEvent({ type: 'tool_result', name: call.name, success, preview: cleanPreview.slice(0, 120) });
|
|
979
739
|
return result;
|
|
980
740
|
}
|
|
981
|
-
async runToolDef(call, toolDef, agentId) {
|
|
982
|
-
const ctx = this.buildContext(agentId);
|
|
741
|
+
async runToolDef(call, toolDef, agentId, depth = 0) {
|
|
742
|
+
const ctx = this.buildContext(agentId, depth);
|
|
983
743
|
try {
|
|
984
744
|
const args = JSON.parse(call.args);
|
|
985
|
-
return await toolDef.execute(args, ctx);
|
|
745
|
+
return { result: await toolDef.execute(args, ctx), success: true };
|
|
986
746
|
}
|
|
987
747
|
catch (err) {
|
|
988
|
-
return `Error: ${err instanceof Error ? err.message : String(err)}
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
/**
|
|
992
|
-
* Returns true when a tool call matches a high-blast-radius pattern that
|
|
993
|
-
* always requires explicit approval — cannot be bypassed by approve_all or
|
|
994
|
-
* auto iteration mode.
|
|
995
|
-
*/
|
|
996
|
-
requiresAlwaysAsk(call) {
|
|
997
|
-
if (call.name !== 'run_command')
|
|
998
|
-
return false;
|
|
999
|
-
try {
|
|
1000
|
-
const { command } = JSON.parse(call.args);
|
|
1001
|
-
if (!command)
|
|
1002
|
-
return false;
|
|
1003
|
-
return AgentEngine.ALWAYS_ASK_PATTERNS.some(re => re.test(command));
|
|
1004
|
-
}
|
|
1005
|
-
catch {
|
|
1006
|
-
return false;
|
|
748
|
+
return { result: `Error: ${err instanceof Error ? err.message : String(err)}`, success: false };
|
|
1007
749
|
}
|
|
1008
750
|
}
|
|
1009
751
|
/**
|
|
@@ -1016,56 +758,13 @@ class AgentEngine {
|
|
|
1016
758
|
* For mode='tools' servers, agents fall back to shell commands automatically.
|
|
1017
759
|
*/
|
|
1018
760
|
unregisterMCPServer(serverName) {
|
|
1019
|
-
|
|
1020
|
-
// Revert tool assignments and command blocks on affected agents
|
|
1021
|
-
const records = this.mcpAgentBlocks.get(safeName) ?? [];
|
|
1022
|
-
for (const { agentId, toolNames, blockedCmds } of records) {
|
|
1023
|
-
const agent = this.opts.agents.get(agentId);
|
|
1024
|
-
if (agent) {
|
|
1025
|
-
this.opts.agents.upsert({
|
|
1026
|
-
...agent,
|
|
1027
|
-
allowedTools: agent.allowedTools.filter(t => !toolNames.includes(t)),
|
|
1028
|
-
blockedCommands: (agent.blockedCommands ?? []).filter(c => !blockedCmds.includes(c)),
|
|
1029
|
-
});
|
|
1030
|
-
}
|
|
1031
|
-
for (const toolName of toolNames) {
|
|
1032
|
-
this.opts.tools.unregister(toolName);
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
this.mcpAgentBlocks.delete(safeName);
|
|
1036
|
-
// Remove wrapper agent if mode='agent'
|
|
1037
|
-
this.opts.agents.delete(safeName);
|
|
1038
|
-
// Clean up routing tables
|
|
1039
|
-
this.mcpTriggers.delete(safeName);
|
|
1040
|
-
for (const [keyword, route] of this.mcpToolTriggers) {
|
|
1041
|
-
if (route.agentId === safeName)
|
|
1042
|
-
this.mcpToolTriggers.delete(keyword);
|
|
1043
|
-
}
|
|
1044
|
-
this.mcpRegisteredServers.delete(serverName);
|
|
1045
|
-
this.opts.transport.emit({
|
|
1046
|
-
type: 'system', level: 'info',
|
|
1047
|
-
text: `[MCP:${serverName}] unregistered — tools removed, commands unblocked.`,
|
|
1048
|
-
});
|
|
761
|
+
this.mcpRegistrar.unregisterServer(serverName);
|
|
1049
762
|
}
|
|
1050
763
|
/** Disable approval gate for the rest of this session (approve_all). */
|
|
1051
|
-
disableToolApproval() { this.
|
|
764
|
+
disableToolApproval() { this.approvalGate.disable(); }
|
|
1052
765
|
/** Re-enable approval gate (e.g. after /iteration manual). */
|
|
1053
|
-
enableToolApproval() { this.
|
|
1054
|
-
|
|
1055
|
-
if (!this.opts.auditLog)
|
|
1056
|
-
return;
|
|
1057
|
-
const line = JSON.stringify({
|
|
1058
|
-
ts: new Date().toISOString(),
|
|
1059
|
-
session: this.opts.sessionId ?? 'default',
|
|
1060
|
-
agent: agentId,
|
|
1061
|
-
tool,
|
|
1062
|
-
input: inputPreview.slice(0, 200),
|
|
1063
|
-
success,
|
|
1064
|
-
durationMs,
|
|
1065
|
-
});
|
|
1066
|
-
(0, fs_1.appendFileSync)(this.opts.auditLog, line + '\n');
|
|
1067
|
-
}
|
|
1068
|
-
buildContext(agentId) {
|
|
766
|
+
enableToolApproval() { this.approvalGate.enable(); }
|
|
767
|
+
buildContext(agentId, depth = 0) {
|
|
1069
768
|
const id = agentId ?? this.currentAgentId;
|
|
1070
769
|
const profile = this.opts.agents.get(id);
|
|
1071
770
|
const storeRoot = this.opts.store?.projectDir;
|
|
@@ -1083,14 +782,14 @@ class AgentEngine {
|
|
|
1083
782
|
...(additionalRoots.length ? { additionalRoots } : {}),
|
|
1084
783
|
...(profile?.blockedCommands?.length ? { blockedCommands: profile.blockedCommands } : {}),
|
|
1085
784
|
askAgent: async (agentId, question, context) => {
|
|
1086
|
-
const result = await this.bus.runSingle({ agentId, question, context });
|
|
785
|
+
const result = await this.bus.runSingle({ agentId, question, context, fromAgentId: id, depth: depth + 1 });
|
|
1087
786
|
return result.content;
|
|
1088
787
|
},
|
|
1089
788
|
emitCompacting: (phase, label) => {
|
|
1090
789
|
this.opts.transport.emit({ type: 'compacting', phase, label });
|
|
1091
790
|
},
|
|
1092
791
|
llmCall: async (prompt) => {
|
|
1093
|
-
const profile = this.opts.agents.get(
|
|
792
|
+
const profile = this.opts.agents.get(id);
|
|
1094
793
|
if (!profile)
|
|
1095
794
|
return '';
|
|
1096
795
|
const provider = this.getProviderForProfile(profile);
|
|
@@ -1107,9 +806,4 @@ class AgentEngine {
|
|
|
1107
806
|
}
|
|
1108
807
|
}
|
|
1109
808
|
exports.AgentEngine = AgentEngine;
|
|
1110
|
-
function toTitleCase(s) {
|
|
1111
|
-
return s
|
|
1112
|
-
.replace(/[-_](.)/g, (_, c) => ' ' + c.toUpperCase())
|
|
1113
|
-
.replace(/^(.)/, (c) => c.toUpperCase());
|
|
1114
|
-
}
|
|
1115
809
|
//# sourceMappingURL=AgentEngine.js.map
|