opc-agent 2.0.0 → 2.0.2
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 +545 -365
- package/dist/channels/email.d.ts +32 -26
- package/dist/channels/email.js +239 -62
- package/dist/channels/feishu.d.ts +21 -6
- package/dist/channels/feishu.js +225 -126
- package/dist/channels/websocket.d.ts +46 -3
- package/dist/channels/websocket.js +306 -37
- package/dist/channels/wechat.d.ts +33 -13
- package/dist/channels/wechat.js +229 -42
- package/dist/cli.js +712 -11
- package/dist/core/a2a.d.ts +17 -0
- package/dist/core/a2a.js +43 -1
- package/dist/core/agent.d.ts +16 -0
- package/dist/core/agent.js +108 -0
- package/dist/core/runtime.d.ts +6 -0
- package/dist/core/runtime.js +161 -2
- package/dist/core/sandbox.d.ts +26 -0
- package/dist/core/sandbox.js +117 -0
- package/dist/core/workflow-graph.d.ts +93 -0
- package/dist/core/workflow-graph.js +247 -0
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.js +183 -0
- package/dist/eval/index.d.ts +65 -0
- package/dist/eval/index.js +191 -0
- package/dist/index.d.ts +32 -6
- package/dist/index.js +63 -4
- package/dist/plugins/content-filter.d.ts +7 -0
- package/dist/plugins/content-filter.js +25 -0
- package/dist/plugins/index.d.ts +42 -0
- package/dist/plugins/index.js +108 -2
- package/dist/plugins/logger.d.ts +6 -0
- package/dist/plugins/logger.js +20 -0
- package/dist/plugins/rate-limiter.d.ts +7 -0
- package/dist/plugins/rate-limiter.js +35 -0
- package/dist/protocols/a2a/client.d.ts +25 -0
- package/dist/protocols/a2a/client.js +115 -0
- package/dist/protocols/a2a/index.d.ts +6 -0
- package/dist/protocols/a2a/index.js +12 -0
- package/dist/protocols/a2a/server.d.ts +41 -0
- package/dist/protocols/a2a/server.js +295 -0
- package/dist/protocols/a2a/types.d.ts +91 -0
- package/dist/protocols/a2a/types.js +15 -0
- package/dist/protocols/a2a/utils.d.ts +6 -0
- package/dist/protocols/a2a/utils.js +47 -0
- package/dist/protocols/agui/client.d.ts +10 -0
- package/dist/protocols/agui/client.js +75 -0
- package/dist/protocols/agui/index.d.ts +4 -0
- package/dist/protocols/agui/index.js +25 -0
- package/dist/protocols/agui/server.d.ts +37 -0
- package/dist/protocols/agui/server.js +191 -0
- package/dist/protocols/agui/types.d.ts +107 -0
- package/dist/protocols/agui/types.js +17 -0
- package/dist/protocols/index.d.ts +2 -0
- package/dist/protocols/index.js +19 -0
- package/dist/protocols/mcp/agent-tools.d.ts +11 -0
- package/dist/protocols/mcp/agent-tools.js +129 -0
- package/dist/protocols/mcp/index.d.ts +5 -0
- package/dist/protocols/mcp/index.js +11 -0
- package/dist/protocols/mcp/server.d.ts +31 -0
- package/dist/protocols/mcp/server.js +248 -0
- package/dist/protocols/mcp/types.d.ts +92 -0
- package/dist/protocols/mcp/types.js +17 -0
- package/dist/publish/index.d.ts +45 -0
- package/dist/publish/index.js +350 -0
- package/dist/schema/oad.d.ts +682 -65
- package/dist/schema/oad.js +36 -3
- package/dist/security/approval.d.ts +36 -0
- package/dist/security/approval.js +113 -0
- package/dist/security/index.d.ts +4 -0
- package/dist/security/index.js +8 -0
- package/dist/security/keys.d.ts +16 -0
- package/dist/security/keys.js +117 -0
- package/dist/studio/server.d.ts +63 -0
- package/dist/studio/server.js +625 -0
- package/dist/studio-ui/index.html +662 -0
- package/dist/telemetry/index.d.ts +93 -0
- package/dist/telemetry/index.js +285 -0
- package/package.json +5 -3
- package/scripts/install.ps1 +31 -0
- package/scripts/install.sh +40 -0
- package/src/channels/email.ts +351 -177
- package/src/channels/feishu.ts +349 -236
- package/src/channels/websocket.ts +399 -87
- package/src/channels/wechat.ts +329 -149
- package/src/cli.ts +783 -12
- package/src/core/a2a.ts +60 -0
- package/src/core/agent.ts +125 -0
- package/src/core/runtime.ts +127 -0
- package/src/core/sandbox.ts +143 -0
- package/src/core/workflow-graph.ts +365 -0
- package/src/doctor.ts +156 -0
- package/src/eval/index.ts +211 -0
- package/src/eval/suites/basic.json +16 -0
- package/src/eval/suites/memory.json +12 -0
- package/src/eval/suites/safety.json +14 -0
- package/src/index.ts +58 -6
- package/src/plugins/content-filter.ts +23 -0
- package/src/plugins/index.ts +133 -2
- package/src/plugins/logger.ts +18 -0
- package/src/plugins/rate-limiter.ts +38 -0
- package/src/protocols/a2a/client.ts +132 -0
- package/src/protocols/a2a/index.ts +8 -0
- package/src/protocols/a2a/server.ts +333 -0
- package/src/protocols/a2a/types.ts +88 -0
- package/src/protocols/a2a/utils.ts +50 -0
- package/src/protocols/agui/client.ts +83 -0
- package/src/protocols/agui/index.ts +4 -0
- package/src/protocols/agui/server.ts +218 -0
- package/src/protocols/agui/types.ts +153 -0
- package/src/protocols/index.ts +2 -0
- package/src/protocols/mcp/agent-tools.ts +134 -0
- package/src/protocols/mcp/index.ts +8 -0
- package/src/protocols/mcp/server.ts +262 -0
- package/src/protocols/mcp/types.ts +69 -0
- package/src/publish/index.ts +376 -0
- package/src/schema/oad.ts +39 -2
- package/src/security/approval.ts +131 -0
- package/src/security/index.ts +3 -0
- package/src/security/keys.ts +87 -0
- package/src/studio/server.ts +629 -0
- package/src/studio-ui/index.html +662 -0
- package/src/telemetry/index.ts +324 -0
- package/src/types/agent-workstation.d.ts +2 -0
- package/tests/a2a-protocol.test.ts +285 -0
- package/tests/agui-protocol.test.ts +246 -0
- package/tests/channels/discord.test.ts +79 -0
- package/tests/channels/email.test.ts +148 -0
- package/tests/channels/feishu.test.ts +123 -0
- package/tests/channels/telegram.test.ts +129 -0
- package/tests/channels/websocket.test.ts +53 -0
- package/tests/channels/wechat.test.ts +170 -0
- package/tests/chat-cli.test.ts +160 -0
- package/tests/daemon.test.ts +135 -0
- package/tests/deepbrain-wire.test.ts +234 -0
- package/tests/doctor.test.ts +38 -0
- package/tests/eval.test.ts +173 -0
- package/tests/init-role.test.ts +124 -0
- package/tests/mcp-client.test.ts +92 -0
- package/tests/mcp-server.test.ts +178 -0
- package/tests/plugin-a2a-enhanced.test.ts +230 -0
- package/tests/publish.test.ts +231 -0
- package/tests/scheduler.test.ts +200 -0
- package/tests/security-enhanced.test.ts +233 -0
- package/tests/skill-learner.test.ts +161 -0
- package/tests/studio.test.ts +229 -0
- package/tests/subagent.test.ts +63 -0
- package/tests/telemetry.test.ts +186 -0
- package/tests/tools/builtin-extended.test.ts +138 -0
- package/tests/workflow-graph.test.ts +279 -0
- package/tutorial/customer-service-agent/README.md +612 -0
- package/tutorial/customer-service-agent/SOUL.md +26 -0
- package/tutorial/customer-service-agent/agent.yaml +63 -0
- package/tutorial/customer-service-agent/package.json +19 -0
- package/tutorial/customer-service-agent/src/index.ts +69 -0
- package/tutorial/customer-service-agent/src/skills/faq.ts +27 -0
- package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -0
- package/tutorial/customer-service-agent/tsconfig.json +14 -0
package/src/core/a2a.ts
CHANGED
|
@@ -3,6 +3,66 @@ import { Room } from './room';
|
|
|
3
3
|
import type { Message, IAgent } from './types';
|
|
4
4
|
import { Logger } from './logger';
|
|
5
5
|
|
|
6
|
+
// ── Agent Card (v1.6.0 — simplified A2A) ────────────────────
|
|
7
|
+
|
|
8
|
+
export interface AgentCard {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
capabilities: string[];
|
|
12
|
+
endpoint?: string;
|
|
13
|
+
handler?: (message: string) => Promise<string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class AgentCardRegistry {
|
|
17
|
+
private cards: Map<string, AgentCard> = new Map();
|
|
18
|
+
private logger = new Logger('a2a:cards');
|
|
19
|
+
|
|
20
|
+
register(card: AgentCard): void {
|
|
21
|
+
this.cards.set(card.name, card);
|
|
22
|
+
this.logger.info('AgentCard registered', { name: card.name });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
unregister(name: string): void {
|
|
26
|
+
this.cards.delete(name);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get(name: string): AgentCard | undefined {
|
|
30
|
+
return this.cards.get(name);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
find(query: string): AgentCard[] {
|
|
34
|
+
const lower = query.toLowerCase();
|
|
35
|
+
return Array.from(this.cards.values()).filter(a =>
|
|
36
|
+
a.name.toLowerCase().includes(lower) ||
|
|
37
|
+
a.description.toLowerCase().includes(lower) ||
|
|
38
|
+
a.capabilities.some(c => c.toLowerCase().includes(lower))
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async send(agentName: string, message: string): Promise<string> {
|
|
43
|
+
const agent = this.cards.get(agentName);
|
|
44
|
+
if (!agent) throw new Error(`Agent '${agentName}' not found`);
|
|
45
|
+
|
|
46
|
+
if (agent.handler) {
|
|
47
|
+
return agent.handler(message);
|
|
48
|
+
} else if (agent.endpoint) {
|
|
49
|
+
const res = await fetch(agent.endpoint, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { 'Content-Type': 'application/json' },
|
|
52
|
+
body: JSON.stringify({ message }),
|
|
53
|
+
});
|
|
54
|
+
const data = await res.json() as any;
|
|
55
|
+
return data.response || data.content || '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
throw new Error(`Agent '${agentName}' has no handler or endpoint`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
list(): AgentCard[] {
|
|
62
|
+
return Array.from(this.cards.values());
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
6
66
|
// ── A2A Types ───────────────────────────────────────────────
|
|
7
67
|
|
|
8
68
|
export interface AgentCapability {
|
package/src/core/agent.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { SkillLearner } from '../skills/auto-learn';
|
|
|
6
6
|
import type { MCPTool } from '../tools/mcp';
|
|
7
7
|
import { MCPToolRegistry } from '../tools/mcp';
|
|
8
8
|
import { SubAgentManager, type SubAgentConfig, type SubAgentResult } from './subagent';
|
|
9
|
+
import { Tracer } from '../telemetry';
|
|
10
|
+
import type { Span as TelemetrySpan } from '../telemetry';
|
|
9
11
|
|
|
10
12
|
export class BaseAgent extends EventEmitter implements IAgent {
|
|
11
13
|
readonly name: string;
|
|
@@ -21,6 +23,9 @@ export class BaseAgent extends EventEmitter implements IAgent {
|
|
|
21
23
|
private skillLearner?: SkillLearner;
|
|
22
24
|
private autoLearnConfig: { enabled: boolean; minConversationLength: number; improveOnUse: boolean };
|
|
23
25
|
private _subAgentManager?: SubAgentManager;
|
|
26
|
+
private longTermMemory?: any;
|
|
27
|
+
private longTermMemoryConfig: { autoLearn: boolean; autoRecall: boolean } = { autoLearn: true, autoRecall: true };
|
|
28
|
+
private tracer?: Tracer;
|
|
24
29
|
|
|
25
30
|
constructor(options: {
|
|
26
31
|
name: string;
|
|
@@ -36,6 +41,7 @@ export class BaseAgent extends EventEmitter implements IAgent {
|
|
|
36
41
|
improveOnUse?: boolean;
|
|
37
42
|
};
|
|
38
43
|
maxToolRounds?: number;
|
|
44
|
+
tracer?: Tracer;
|
|
39
45
|
}) {
|
|
40
46
|
super();
|
|
41
47
|
this.name = options.name;
|
|
@@ -52,6 +58,25 @@ export class BaseAgent extends EventEmitter implements IAgent {
|
|
|
52
58
|
if (options.skillsDir) {
|
|
53
59
|
this.skillLearner = new SkillLearner(options.skillsDir);
|
|
54
60
|
}
|
|
61
|
+
this.tracer = options.tracer;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setLongTermMemory(brain: any, config?: { autoLearn?: boolean; autoRecall?: boolean }): void {
|
|
65
|
+
this.longTermMemory = brain;
|
|
66
|
+
if (config) {
|
|
67
|
+
this.longTermMemoryConfig = {
|
|
68
|
+
autoLearn: config.autoLearn !== false,
|
|
69
|
+
autoRecall: config.autoRecall !== false,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getLongTermMemory(): any {
|
|
75
|
+
return this.longTermMemory;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getLongTermMemoryConfig(): { autoLearn: boolean; autoRecall: boolean } {
|
|
79
|
+
return this.longTermMemoryConfig;
|
|
55
80
|
}
|
|
56
81
|
|
|
57
82
|
get state(): AgentState {
|
|
@@ -78,6 +103,14 @@ export class BaseAgent extends EventEmitter implements IAgent {
|
|
|
78
103
|
return this.toolRegistry;
|
|
79
104
|
}
|
|
80
105
|
|
|
106
|
+
getTracer(): Tracer | undefined {
|
|
107
|
+
return this.tracer;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
setTracer(tracer: Tracer): void {
|
|
111
|
+
this.tracer = tracer;
|
|
112
|
+
}
|
|
113
|
+
|
|
81
114
|
registerTool(tool: MCPTool): void {
|
|
82
115
|
this.toolRegistry.register(tool);
|
|
83
116
|
}
|
|
@@ -143,9 +176,45 @@ export class BaseAgent extends EventEmitter implements IAgent {
|
|
|
143
176
|
async handleMessage(message: Message): Promise<Message> {
|
|
144
177
|
this.emit('message:in', message);
|
|
145
178
|
|
|
179
|
+
// Start root span if tracer is configured
|
|
180
|
+
let rootSpan: TelemetrySpan | undefined;
|
|
181
|
+
if (this.tracer) {
|
|
182
|
+
rootSpan = this.tracer.startSpan('handleMessage', {
|
|
183
|
+
kind: 'server',
|
|
184
|
+
attributes: {
|
|
185
|
+
'message.channel': (message.metadata?.channel as string) || 'unknown',
|
|
186
|
+
'message.sender': (message.metadata?.sender as string) || 'unknown',
|
|
187
|
+
'message.length': message.content.length,
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
this.tracer.increment('agent.messages.total', 1, { agent: this.name });
|
|
191
|
+
}
|
|
192
|
+
|
|
146
193
|
const sessionId = (message.metadata?.sessionId as string) ?? 'default';
|
|
147
194
|
await this.memory.addMessage(sessionId, message);
|
|
148
195
|
|
|
196
|
+
// === Recall from long-term memory ===
|
|
197
|
+
let memoryContext = '';
|
|
198
|
+
if (this.longTermMemory && this.longTermMemoryConfig.autoRecall) {
|
|
199
|
+
let memorySpan: TelemetrySpan | undefined;
|
|
200
|
+
if (this.tracer && rootSpan) {
|
|
201
|
+
memorySpan = this.tracer.startSpan('memory.recall', { parent: rootSpan, kind: 'client' });
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
const recalled = await this.longTermMemory.recall(message.content);
|
|
205
|
+
if (recalled && (Array.isArray(recalled) ? recalled.length > 0 : true)) {
|
|
206
|
+
memoryContext = '\n\n[Relevant memories]\n' +
|
|
207
|
+
(Array.isArray(recalled)
|
|
208
|
+
? recalled.map((r: any) => typeof r === 'string' ? r : r.content || r.compiled_truth || '').join('\n')
|
|
209
|
+
: String(recalled));
|
|
210
|
+
}
|
|
211
|
+
if (this.tracer && memorySpan) this.tracer.endSpan(memorySpan, 'ok');
|
|
212
|
+
} catch {
|
|
213
|
+
if (this.tracer && memorySpan) this.tracer.endSpan(memorySpan, 'error');
|
|
214
|
+
// Silent fail — don't break chat if memory fails
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
149
218
|
const context: AgentContext = {
|
|
150
219
|
agentName: this.name,
|
|
151
220
|
sessionId,
|
|
@@ -172,12 +241,21 @@ export class BaseAgent extends EventEmitter implements IAgent {
|
|
|
172
241
|
|
|
173
242
|
// Check if a learned skill matches — prepend instructions to system prompt
|
|
174
243
|
let effectiveSystemPrompt = this.systemPrompt;
|
|
244
|
+
|
|
245
|
+
// Inject long-term memory context
|
|
246
|
+
if (memoryContext) {
|
|
247
|
+
effectiveSystemPrompt = effectiveSystemPrompt + memoryContext;
|
|
248
|
+
}
|
|
249
|
+
|
|
175
250
|
const matchedSkill = this.skillLearner?.matchSkill(message.content);
|
|
176
251
|
if (matchedSkill) {
|
|
177
252
|
matchedSkill.usageCount++;
|
|
178
253
|
matchedSkill.lastUsed = new Date();
|
|
179
254
|
effectiveSystemPrompt = `[Learned Skill: ${matchedSkill.name}]\n${matchedSkill.instructions}\n\n${this.systemPrompt}`;
|
|
180
255
|
this.emit('skill:matched', matchedSkill);
|
|
256
|
+
if (this.tracer && rootSpan) {
|
|
257
|
+
this.tracer.addEvent(rootSpan, 'skill.matched', { 'skill.name': matchedSkill.name });
|
|
258
|
+
}
|
|
181
259
|
}
|
|
182
260
|
|
|
183
261
|
// Fall back to LLM with tool use loop
|
|
@@ -186,12 +264,26 @@ export class BaseAgent extends EventEmitter implements IAgent {
|
|
|
186
264
|
let finalResponse = '';
|
|
187
265
|
|
|
188
266
|
for (let round = 0; round <= this.maxToolRounds; round++) {
|
|
267
|
+
let llmSpan: TelemetrySpan | undefined;
|
|
268
|
+
if (this.tracer && rootSpan) {
|
|
269
|
+
llmSpan = this.tracer.startSpan('llm.chat', {
|
|
270
|
+
parent: rootSpan,
|
|
271
|
+
kind: 'client',
|
|
272
|
+
attributes: { 'llm.round': round },
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
189
276
|
const llmResponse = await this._provider.chat(
|
|
190
277
|
llmMessages,
|
|
191
278
|
effectiveSystemPrompt,
|
|
192
279
|
{ tools: tools.length > 0 ? tools : undefined },
|
|
193
280
|
);
|
|
194
281
|
|
|
282
|
+
if (this.tracer && llmSpan) {
|
|
283
|
+
llmSpan.attributes['llm.response.length'] = llmResponse.length;
|
|
284
|
+
this.tracer.endSpan(llmSpan, 'ok');
|
|
285
|
+
}
|
|
286
|
+
|
|
195
287
|
const toolCall = this.parseToolCall(llmResponse);
|
|
196
288
|
if (!toolCall || tools.length === 0 || round === this.maxToolRounds) {
|
|
197
289
|
finalResponse = llmResponse;
|
|
@@ -199,9 +291,23 @@ export class BaseAgent extends EventEmitter implements IAgent {
|
|
|
199
291
|
}
|
|
200
292
|
|
|
201
293
|
// Execute tool
|
|
294
|
+
let toolSpan: TelemetrySpan | undefined;
|
|
295
|
+
if (this.tracer && rootSpan) {
|
|
296
|
+
toolSpan = this.tracer.startSpan('tool.execute', {
|
|
297
|
+
parent: rootSpan,
|
|
298
|
+
kind: 'internal',
|
|
299
|
+
attributes: { 'tool.name': toolCall.name },
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
202
303
|
const toolResult = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
|
|
203
304
|
this.emit('tool:execute', toolCall.name, toolResult);
|
|
204
305
|
|
|
306
|
+
if (this.tracer && toolSpan) {
|
|
307
|
+
toolSpan.attributes['tool.result.length'] = toolResult.content?.length || 0;
|
|
308
|
+
this.tracer.endSpan(toolSpan, 'ok');
|
|
309
|
+
}
|
|
310
|
+
|
|
205
311
|
// Add tool call and result to messages for next round
|
|
206
312
|
llmMessages.push({
|
|
207
313
|
id: `tool_call_${Date.now()}`,
|
|
@@ -221,6 +327,25 @@ export class BaseAgent extends EventEmitter implements IAgent {
|
|
|
221
327
|
await this.memory.addMessage(sessionId, response);
|
|
222
328
|
this.emit('message:out', response);
|
|
223
329
|
|
|
330
|
+
// End root telemetry span
|
|
331
|
+
if (this.tracer && rootSpan) {
|
|
332
|
+
rootSpan.attributes['response.length'] = finalResponse.length;
|
|
333
|
+
this.tracer.endSpan(rootSpan, 'ok');
|
|
334
|
+
this.tracer.histogram('agent.message.duration', rootSpan.endTime! - rootSpan.startTime, { agent: this.name });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// === Learn from interaction ===
|
|
338
|
+
if (this.longTermMemory && this.longTermMemoryConfig.autoLearn) {
|
|
339
|
+
try {
|
|
340
|
+
await this.longTermMemory.learn(
|
|
341
|
+
`User: ${message.content}\nAssistant: ${finalResponse}`,
|
|
342
|
+
{ tags: ['conversation', (message.metadata?.channel as string) || 'unknown'] },
|
|
343
|
+
);
|
|
344
|
+
} catch {
|
|
345
|
+
// Silent fail
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
224
349
|
// After response, check if we should learn a skill
|
|
225
350
|
if (
|
|
226
351
|
this.skillLearner &&
|
package/src/core/runtime.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
import { PluginManager } from '../plugins';
|
|
2
|
+
import type { Plugin } from '../plugins';
|
|
3
|
+
import { loggerPlugin } from '../plugins/logger';
|
|
4
|
+
import { createRateLimiterPlugin } from '../plugins/rate-limiter';
|
|
5
|
+
import { createContentFilterPlugin } from '../plugins/content-filter';
|
|
1
6
|
import { BaseAgent } from './agent';
|
|
2
7
|
import { loadOAD } from './config';
|
|
3
8
|
import { Logger } from './logger';
|
|
4
9
|
import { WebChannel } from '../channels/web';
|
|
5
10
|
import { TelegramChannel } from '../channels/telegram';
|
|
6
11
|
import { WebSocketChannel } from '../channels/websocket';
|
|
12
|
+
import { WeChatChannel } from '../channels/wechat';
|
|
13
|
+
import { FeishuChannel } from '../channels/feishu';
|
|
14
|
+
import { EmailChannel } from '../channels/email';
|
|
7
15
|
import { DeepBrainMemoryStore } from '../memory/deepbrain';
|
|
8
16
|
import { Analytics } from '../analytics';
|
|
9
17
|
import type { OADDocument } from '../schema/oad';
|
|
@@ -30,6 +38,10 @@ export class AgentRuntime {
|
|
|
30
38
|
private isShuttingDown = false;
|
|
31
39
|
private analytics: Analytics = new Analytics();
|
|
32
40
|
private scheduler: Scheduler | null = null;
|
|
41
|
+
private pluginManager: PluginManager = new PluginManager();
|
|
42
|
+
private brain: any = null;
|
|
43
|
+
private agentBrain: any = null;
|
|
44
|
+
private evolveScheduler: any = null;
|
|
33
45
|
|
|
34
46
|
async loadConfig(filePath: string): Promise<OADDocument> {
|
|
35
47
|
this.config = loadOAD(filePath);
|
|
@@ -108,11 +120,93 @@ export class AgentRuntime {
|
|
|
108
120
|
} else if (ch.type === 'websocket') {
|
|
109
121
|
this.agent.bindChannel(new WebSocketChannel(ch.port ?? 3002));
|
|
110
122
|
this.logger.info('Bound websocket channel', { port: ch.port ?? 3002 });
|
|
123
|
+
} else if (ch.type === 'wechat') {
|
|
124
|
+
this.agent.bindChannel(new WeChatChannel({
|
|
125
|
+
appId: (ch.config?.appId as string) ?? process.env.WECHAT_APP_ID ?? '',
|
|
126
|
+
appSecret: (ch.config?.appSecret as string) ?? process.env.WECHAT_APP_SECRET ?? '',
|
|
127
|
+
token: (ch.config?.token as string) ?? process.env.WECHAT_TOKEN ?? '',
|
|
128
|
+
encodingAESKey: ch.config?.encodingAESKey as string,
|
|
129
|
+
port: ch.port,
|
|
130
|
+
}));
|
|
131
|
+
this.logger.info('Bound wechat channel', { port: ch.port ?? 8080 });
|
|
132
|
+
} else if (ch.type === 'feishu') {
|
|
133
|
+
this.agent.bindChannel(new FeishuChannel({
|
|
134
|
+
appId: (ch.config?.appId as string) ?? process.env.FEISHU_APP_ID,
|
|
135
|
+
appSecret: (ch.config?.appSecret as string) ?? process.env.FEISHU_APP_SECRET,
|
|
136
|
+
verificationToken: (ch.config?.verificationToken as string) ?? process.env.FEISHU_VERIFICATION_TOKEN,
|
|
137
|
+
encryptKey: ch.config?.encryptKey as string,
|
|
138
|
+
port: ch.port,
|
|
139
|
+
}));
|
|
140
|
+
this.logger.info('Bound feishu channel', { port: ch.port ?? 8081 });
|
|
141
|
+
} else if (ch.type === 'email') {
|
|
142
|
+
this.agent.bindChannel(new EmailChannel({
|
|
143
|
+
mode: (ch.config?.mode as 'webhook' | 'imap') ?? 'webhook',
|
|
144
|
+
smtp: ch.config?.smtp as any,
|
|
145
|
+
imap: ch.config?.imap as any,
|
|
146
|
+
webhookPort: ch.port,
|
|
147
|
+
filters: ch.config?.filters as any,
|
|
148
|
+
}));
|
|
149
|
+
this.logger.info('Bound email channel', { mode: ch.config?.mode ?? 'webhook', port: ch.port ?? 8082 });
|
|
111
150
|
}
|
|
112
151
|
}
|
|
113
152
|
|
|
114
153
|
await this.agent.init();
|
|
115
154
|
|
|
155
|
+
// === Auto-wire DeepBrain long-term memory (Brain/AgentBrain) ===
|
|
156
|
+
const longTermCfg = memCfg && typeof memCfg.longTerm === 'object' ? memCfg.longTerm : null;
|
|
157
|
+
if (longTermCfg?.provider === 'deepbrain') {
|
|
158
|
+
try {
|
|
159
|
+
const deepbrainModule = await import(/* webpackIgnore: true */ 'deepbrain');
|
|
160
|
+
const BrainClass = deepbrainModule.Brain ?? deepbrainModule.default?.Brain;
|
|
161
|
+
const AgentBrainClass = deepbrainModule.AgentBrain ?? deepbrainModule.default?.AgentBrain;
|
|
162
|
+
|
|
163
|
+
if (BrainClass && AgentBrainClass) {
|
|
164
|
+
const dbConfig = longTermCfg.config ?? {};
|
|
165
|
+
const dbPath = (dbConfig as any).database || './data/brain.db';
|
|
166
|
+
const embeddingProvider = (dbConfig as any).embeddingProvider || 'ollama';
|
|
167
|
+
|
|
168
|
+
this.brain = new BrainClass({
|
|
169
|
+
database: dbPath,
|
|
170
|
+
embedding_provider: embeddingProvider,
|
|
171
|
+
});
|
|
172
|
+
await this.brain.connect();
|
|
173
|
+
|
|
174
|
+
this.agentBrain = new AgentBrainClass(this.brain, cfg.metadata.name);
|
|
175
|
+
this.agent.setLongTermMemory(this.agentBrain, {
|
|
176
|
+
autoLearn: (dbConfig as any).autoLearn !== false,
|
|
177
|
+
autoRecall: (dbConfig as any).autoRecall !== false,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
this.logger.info('DeepBrain Brain/AgentBrain connected', { database: dbPath });
|
|
181
|
+
|
|
182
|
+
// Brain seed loading
|
|
183
|
+
const { existsSync, readFileSync, renameSync } = await import('fs');
|
|
184
|
+
const seedPath = './data/brain-seed.md';
|
|
185
|
+
if (existsSync(seedPath)) {
|
|
186
|
+
const seed = readFileSync(seedPath, 'utf-8');
|
|
187
|
+
await this.brain.put('brain-seed', seed, { type: 'seed', tags: ['seed', 'initial'] });
|
|
188
|
+
renameSync(seedPath, './data/brain-seed.loaded.md');
|
|
189
|
+
this.logger.info('Brain seed loaded');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Auto-evolve scheduling
|
|
193
|
+
const evolveInterval = (dbConfig as any).evolveInterval;
|
|
194
|
+
if (evolveInterval && evolveInterval > 0) {
|
|
195
|
+
const AutoEvolveSchedulerClass = deepbrainModule.AutoEvolveScheduler ?? deepbrainModule.default?.AutoEvolveScheduler;
|
|
196
|
+
if (AutoEvolveSchedulerClass) {
|
|
197
|
+
this.evolveScheduler = new AutoEvolveSchedulerClass();
|
|
198
|
+
this.evolveScheduler.start(this.agentBrain, evolveInterval);
|
|
199
|
+
this.logger.info('DeepBrain auto-evolve scheduled', { interval: evolveInterval });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
this.logger.warn('DeepBrain module found but Brain/AgentBrain classes not available');
|
|
204
|
+
}
|
|
205
|
+
} catch (e: any) {
|
|
206
|
+
this.logger.warn('DeepBrain not available (install with: npm install deepbrain)', { error: e.message });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
116
210
|
// Wire analytics to agent events
|
|
117
211
|
this.agent.on('message:out', () => {
|
|
118
212
|
// responseTime is approximated; real timing is done via skill/llm events
|
|
@@ -126,6 +220,24 @@ export class AgentRuntime {
|
|
|
126
220
|
|
|
127
221
|
this.logger.info('Agent initialized', { name: cfg.metadata.name });
|
|
128
222
|
|
|
223
|
+
// Load enhanced plugins from OAD config
|
|
224
|
+
const pluginsCfg = (cfg.spec as any).plugins;
|
|
225
|
+
if (pluginsCfg && Array.isArray(pluginsCfg)) {
|
|
226
|
+
const builtinPlugins: Record<string, (config?: any) => Plugin> = {
|
|
227
|
+
'logger': () => loggerPlugin,
|
|
228
|
+
'rate-limiter': (c: any) => createRateLimiterPlugin(c?.maxPerMinute ?? 60),
|
|
229
|
+
'content-filter': (c: any) => createContentFilterPlugin(c?.blocklist ?? []),
|
|
230
|
+
};
|
|
231
|
+
for (const entry of pluginsCfg) {
|
|
232
|
+
const factory = builtinPlugins[entry.name];
|
|
233
|
+
if (factory) {
|
|
234
|
+
this.pluginManager.registerEnhanced(factory(entry.config));
|
|
235
|
+
this.logger.info('Enhanced plugin loaded from config', { name: entry.name });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
await this.pluginManager.initAll(this);
|
|
240
|
+
|
|
129
241
|
// Initialize scheduler if jobs are configured
|
|
130
242
|
const schedulerCfg = (cfg.spec as any).scheduler;
|
|
131
243
|
if (schedulerCfg?.jobs && Array.isArray(schedulerCfg.jobs) && schedulerCfg.jobs.length > 0) {
|
|
@@ -178,10 +290,21 @@ export class AgentRuntime {
|
|
|
178
290
|
async stop(): Promise<void> {
|
|
179
291
|
if (!this.agent) return;
|
|
180
292
|
this.logger.info('Stopping agent...');
|
|
293
|
+
if (this.evolveScheduler) {
|
|
294
|
+
try { this.evolveScheduler.stop(); } catch { /* ignore */ }
|
|
295
|
+
this.logger.info('DeepBrain auto-evolve stopped');
|
|
296
|
+
}
|
|
297
|
+
if (this.brain) {
|
|
298
|
+
try {
|
|
299
|
+
await this.brain.disconnect();
|
|
300
|
+
this.logger.info('DeepBrain disconnected');
|
|
301
|
+
} catch { /* ignore */ }
|
|
302
|
+
}
|
|
181
303
|
if (this.scheduler) {
|
|
182
304
|
this.scheduler.stop();
|
|
183
305
|
this.logger.info('Scheduler stopped');
|
|
184
306
|
}
|
|
307
|
+
await this.pluginManager.shutdownAll();
|
|
185
308
|
await this.agent.stop();
|
|
186
309
|
for (const handler of this.shutdownHandlers) {
|
|
187
310
|
await handler();
|
|
@@ -227,4 +350,8 @@ export class AgentRuntime {
|
|
|
227
350
|
getConfig(): OADDocument | null {
|
|
228
351
|
return this.config;
|
|
229
352
|
}
|
|
353
|
+
|
|
354
|
+
getPluginManager(): PluginManager {
|
|
355
|
+
return this.pluginManager;
|
|
356
|
+
}
|
|
230
357
|
}
|
package/src/core/sandbox.ts
CHANGED
|
@@ -1,11 +1,30 @@
|
|
|
1
1
|
import type { TrustLevelType } from '../schema/oad';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs';
|
|
3
4
|
|
|
4
5
|
export interface SandboxConfig {
|
|
5
6
|
trustLevel: TrustLevelType;
|
|
6
7
|
agentDir: string;
|
|
7
8
|
networkAllowlist?: string[];
|
|
8
9
|
shellAllowed?: boolean;
|
|
10
|
+
allowedCommands?: string[];
|
|
11
|
+
blockedCommands?: string[];
|
|
12
|
+
maxFileSize?: number; // bytes, default 10MB
|
|
13
|
+
maxFiles?: number; // max files in workspace, default 1000
|
|
14
|
+
networkAccess?: boolean; // allow network, default true
|
|
15
|
+
readOnlyPaths?: string[]; // paths that can't be written
|
|
16
|
+
timeout?: number; // global timeout ms
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ValidationResult {
|
|
20
|
+
allowed: boolean;
|
|
21
|
+
reason?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SandboxStatus {
|
|
25
|
+
files: number;
|
|
26
|
+
totalSize: number;
|
|
27
|
+
violations: number;
|
|
9
28
|
}
|
|
10
29
|
|
|
11
30
|
export interface SandboxRestrictions {
|
|
@@ -40,6 +59,9 @@ const TRUST_RESTRICTIONS: Record<string, SandboxRestrictions> = {
|
|
|
40
59
|
export class Sandbox {
|
|
41
60
|
private config: SandboxConfig;
|
|
42
61
|
private restrictions: SandboxRestrictions;
|
|
62
|
+
private violations: number = 0;
|
|
63
|
+
private maxFileSize: number;
|
|
64
|
+
private maxFiles: number;
|
|
43
65
|
|
|
44
66
|
constructor(config: SandboxConfig) {
|
|
45
67
|
this.config = config;
|
|
@@ -52,6 +74,11 @@ export class Sandbox {
|
|
|
52
74
|
if (config.shellAllowed !== undefined) {
|
|
53
75
|
this.restrictions.shell = config.shellAllowed;
|
|
54
76
|
}
|
|
77
|
+
if (config.networkAccess === false) {
|
|
78
|
+
this.restrictions.network.allowed = [];
|
|
79
|
+
}
|
|
80
|
+
this.maxFileSize = config.maxFileSize ?? 10 * 1024 * 1024; // 10MB
|
|
81
|
+
this.maxFiles = config.maxFiles ?? 1000;
|
|
55
82
|
}
|
|
56
83
|
|
|
57
84
|
get trustLevel(): TrustLevelType {
|
|
@@ -98,4 +125,120 @@ export class Sandbox {
|
|
|
98
125
|
checkShellAccess(): boolean {
|
|
99
126
|
return this.restrictions.shell;
|
|
100
127
|
}
|
|
128
|
+
|
|
129
|
+
validateFileOp(action: 'read' | 'write' | 'delete', filePath: string): ValidationResult {
|
|
130
|
+
const resolved = path.resolve(filePath);
|
|
131
|
+
|
|
132
|
+
if (action === 'write' || action === 'delete') {
|
|
133
|
+
// Check read-only paths
|
|
134
|
+
if (this.config.readOnlyPaths) {
|
|
135
|
+
for (const ro of this.config.readOnlyPaths) {
|
|
136
|
+
const roResolved = path.resolve(ro);
|
|
137
|
+
if (resolved.startsWith(roResolved) || resolved === roResolved) {
|
|
138
|
+
this.violations++;
|
|
139
|
+
return { allowed: false, reason: `Path is read-only: ${ro}` };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Check file size for writes
|
|
145
|
+
if (action === 'write') {
|
|
146
|
+
try {
|
|
147
|
+
if (fs.existsSync(resolved)) {
|
|
148
|
+
const stat = fs.statSync(resolved);
|
|
149
|
+
if (stat.size > this.maxFileSize) {
|
|
150
|
+
this.violations++;
|
|
151
|
+
return { allowed: false, reason: `File exceeds max size: ${this.maxFileSize} bytes` };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// File doesn't exist yet — that's fine
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const mode = action === 'read' ? 'read' : 'write';
|
|
161
|
+
if (!this.checkFileAccess(filePath, mode)) {
|
|
162
|
+
this.violations++;
|
|
163
|
+
return { allowed: false, reason: `File access denied for ${action}: ${filePath}` };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { allowed: true };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
validateCommand(command: string): ValidationResult {
|
|
170
|
+
if (!this.restrictions.shell) {
|
|
171
|
+
this.violations++;
|
|
172
|
+
return { allowed: false, reason: 'Shell access is disabled' };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check blocklist
|
|
176
|
+
if (this.config.blockedCommands) {
|
|
177
|
+
for (const blocked of this.config.blockedCommands) {
|
|
178
|
+
if (command.includes(blocked)) {
|
|
179
|
+
this.violations++;
|
|
180
|
+
return { allowed: false, reason: `Command is blocked: ${blocked}` };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check allowlist (if set, only allowed commands pass)
|
|
186
|
+
if (this.config.allowedCommands && this.config.allowedCommands.length > 0) {
|
|
187
|
+
const allowed = this.config.allowedCommands.some(a => command.startsWith(a) || command.includes(a));
|
|
188
|
+
if (!allowed) {
|
|
189
|
+
this.violations++;
|
|
190
|
+
return { allowed: false, reason: 'Command not in allowlist' };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { allowed: true };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
validateNetwork(url: string): ValidationResult {
|
|
198
|
+
if (this.config.networkAccess === false) {
|
|
199
|
+
this.violations++;
|
|
200
|
+
return { allowed: false, reason: 'Network access is disabled' };
|
|
201
|
+
}
|
|
202
|
+
if (!this.checkNetworkAccess(url)) {
|
|
203
|
+
this.violations++;
|
|
204
|
+
return { allowed: false, reason: `Network access denied for: ${url}` };
|
|
205
|
+
}
|
|
206
|
+
return { allowed: true };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
getStatus(): SandboxStatus {
|
|
210
|
+
let files = 0;
|
|
211
|
+
let totalSize = 0;
|
|
212
|
+
try {
|
|
213
|
+
const agentDir = path.resolve(this.config.agentDir);
|
|
214
|
+
if (fs.existsSync(agentDir)) {
|
|
215
|
+
const countFiles = (dir: string) => {
|
|
216
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
217
|
+
for (const entry of entries) {
|
|
218
|
+
const full = path.join(dir, entry.name);
|
|
219
|
+
if (entry.isDirectory() && entry.name !== 'node_modules') {
|
|
220
|
+
countFiles(full);
|
|
221
|
+
} else if (entry.isFile()) {
|
|
222
|
+
files++;
|
|
223
|
+
try { totalSize += fs.statSync(full).size; } catch {}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
countFiles(agentDir);
|
|
228
|
+
}
|
|
229
|
+
} catch {}
|
|
230
|
+
return { files, totalSize, violations: this.violations };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
getViolations(): number {
|
|
234
|
+
return this.violations;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
getMaxFileSize(): number {
|
|
238
|
+
return this.maxFileSize;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
getMaxFiles(): number {
|
|
242
|
+
return this.maxFiles;
|
|
243
|
+
}
|
|
101
244
|
}
|