opc-agent 4.1.0 → 4.1.1
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/.github/ISSUE_TEMPLATE/bug_report.md +20 -20
- package/.github/ISSUE_TEMPLATE/feature_request.md +14 -14
- package/.github/PULL_REQUEST_TEMPLATE.md +13 -13
- package/CHANGELOG.md +48 -48
- package/CONTRIBUTING.md +36 -36
- package/README.zh-CN.md +497 -497
- package/dist/channels/wechat.js +6 -6
- package/dist/deploy/index.js +56 -56
- package/dist/studio/server.js +30 -1
- package/dist/studio-ui/index.html +230 -10
- package/dist/ui/components.js +105 -105
- package/examples/README.md +22 -22
- package/examples/basic-agent.ts +90 -90
- package/examples/brain-integration.ts +71 -71
- package/examples/multi-channel.ts +74 -74
- package/fix-sidebar.mjs +188 -188
- package/install.ps1 +154 -154
- package/install.sh +164 -164
- package/package.json +1 -1
- package/scripts/install.ps1 +31 -31
- package/scripts/install.sh +40 -40
- package/serve-studio.js +13 -13
- package/serve-test.js +25 -25
- package/src/channels/dingtalk.ts +46 -46
- package/src/channels/email.ts +351 -351
- package/src/channels/feishu.ts +349 -349
- package/src/channels/googlechat.ts +42 -42
- package/src/channels/imessage.ts +31 -31
- package/src/channels/irc.ts +82 -82
- package/src/channels/line.ts +32 -32
- package/src/channels/matrix.ts +33 -33
- package/src/channels/mattermost.ts +57 -57
- package/src/channels/msteams.ts +32 -32
- package/src/channels/nostr.ts +32 -32
- package/src/channels/qq.ts +33 -33
- package/src/channels/signal.ts +32 -32
- package/src/channels/sms.ts +33 -33
- package/src/channels/telegram.ts +616 -616
- package/src/channels/twitch.ts +65 -65
- package/src/channels/voice-call.ts +100 -100
- package/src/channels/websocket.ts +399 -399
- package/src/channels/wechat.ts +329 -329
- package/src/channels/whatsapp.ts +32 -32
- package/src/cli/chat.ts +99 -99
- package/src/cli/setup.ts +314 -314
- package/src/core/agent.ts +476 -476
- package/src/core/api-server.ts +277 -277
- package/src/core/audio.ts +98 -98
- package/src/core/collaboration.ts +275 -275
- package/src/core/context-discovery.ts +85 -85
- package/src/core/context-refs.ts +140 -140
- package/src/core/gateway.ts +106 -106
- package/src/core/heartbeat.ts +51 -51
- package/src/core/hooks.ts +105 -105
- package/src/core/ide-bridge.ts +133 -133
- package/src/core/node-network.ts +86 -86
- package/src/core/profiles.ts +122 -122
- package/src/core/scheduler.ts +187 -187
- package/src/core/session-manager.ts +137 -137
- package/src/core/subagent.ts +98 -98
- package/src/core/vision.ts +180 -180
- package/src/core/workflow-graph.ts +365 -365
- package/src/daemon.ts +96 -96
- package/src/deploy/index.ts +255 -255
- package/src/doctor.ts +156 -156
- package/src/eval/index.ts +211 -211
- package/src/eval/suites/basic.json +16 -16
- package/src/eval/suites/memory.json +12 -12
- package/src/eval/suites/safety.json +14 -14
- package/src/hub/brain-seed.ts +54 -54
- package/src/hub/client.ts +60 -60
- package/src/mcp/servers/calculator-mcp.ts +65 -65
- package/src/mcp/servers/crypto-mcp.ts +73 -73
- package/src/mcp/servers/database-mcp.ts +72 -72
- package/src/mcp/servers/datetime-mcp.ts +69 -69
- package/src/mcp/servers/filesystem.ts +66 -66
- package/src/mcp/servers/github-mcp.ts +58 -58
- package/src/mcp/servers/index.ts +63 -63
- package/src/mcp/servers/json-mcp.ts +102 -102
- package/src/mcp/servers/memory-mcp.ts +56 -56
- package/src/mcp/servers/regex-mcp.ts +53 -53
- package/src/mcp/servers/web-mcp.ts +49 -49
- package/src/memory/context-compressor.ts +189 -189
- package/src/memory/seed-loader.ts +212 -212
- package/src/memory/user-profiler.ts +215 -215
- package/src/plugins/content-filter.ts +23 -23
- package/src/plugins/logger.ts +18 -18
- package/src/plugins/rate-limiter.ts +38 -38
- package/src/protocols/a2a/client.ts +132 -132
- package/src/protocols/a2a/index.ts +8 -8
- package/src/protocols/a2a/server.ts +333 -333
- package/src/protocols/a2a/types.ts +88 -88
- package/src/protocols/a2a/utils.ts +50 -50
- package/src/protocols/agui/client.ts +83 -83
- package/src/protocols/agui/index.ts +4 -4
- package/src/protocols/agui/server.ts +218 -218
- package/src/protocols/agui/types.ts +153 -153
- package/src/protocols/index.ts +2 -2
- package/src/protocols/mcp/agent-tools.ts +134 -134
- package/src/protocols/mcp/index.ts +8 -8
- package/src/protocols/mcp/server.ts +262 -262
- package/src/protocols/mcp/types.ts +69 -69
- package/src/providers/index.ts +632 -632
- package/src/publish/index.ts +376 -376
- package/src/scheduler/cron-engine.ts +191 -191
- package/src/scheduler/index.ts +2 -2
- package/src/schema/oad.ts +217 -217
- package/src/security/approval.ts +131 -131
- package/src/security/approvals.ts +143 -143
- package/src/security/elevated.ts +105 -105
- package/src/security/guardrails.ts +248 -248
- package/src/security/index.ts +9 -9
- package/src/security/keys.ts +87 -87
- package/src/security/secrets.ts +129 -129
- package/src/skills/builtin/index.ts +408 -408
- package/src/skills/marketplace.ts +113 -113
- package/src/skills/types.ts +42 -42
- package/src/studio/server.ts +31 -1
- package/src/studio/templates-data.ts +178 -178
- package/src/studio-ui/index.html +230 -10
- package/src/telemetry/index.ts +324 -324
- package/src/tools/builtin/browser.ts +299 -299
- package/src/tools/builtin/datetime.ts +41 -41
- package/src/tools/builtin/file.ts +107 -107
- package/src/tools/builtin/home-assistant.ts +116 -116
- package/src/tools/builtin/rl-tools.ts +243 -243
- package/src/tools/builtin/shell.ts +43 -43
- package/src/tools/builtin/vision.ts +64 -64
- package/src/tools/builtin/web-search.ts +126 -126
- package/src/tools/builtin/web.ts +35 -35
- package/src/tools/document-processor.ts +213 -213
- package/src/tools/image-generator.ts +150 -150
- package/src/tools/integrations/calendar.ts +73 -73
- package/src/tools/integrations/code-exec.ts +39 -39
- package/src/tools/integrations/csv-analyzer.ts +92 -92
- package/src/tools/integrations/database.ts +44 -44
- package/src/tools/integrations/email-send.ts +76 -76
- package/src/tools/integrations/git-tool.ts +42 -42
- package/src/tools/integrations/github-tool.ts +76 -76
- package/src/tools/integrations/image-gen.ts +56 -56
- package/src/tools/integrations/index.ts +92 -92
- package/src/tools/integrations/jira.ts +83 -83
- package/src/tools/integrations/notion.ts +71 -71
- package/src/tools/integrations/npm-tool.ts +48 -48
- package/src/tools/integrations/pdf-reader.ts +58 -58
- package/src/tools/integrations/slack.ts +65 -65
- package/src/tools/integrations/summarizer.ts +49 -49
- package/src/tools/integrations/translator.ts +48 -48
- package/src/tools/integrations/trello.ts +60 -60
- package/src/tools/integrations/vector-search.ts +42 -42
- package/src/tools/integrations/web-scraper.ts +47 -47
- package/src/tools/integrations/web-search.ts +58 -58
- package/src/tools/integrations/webhook.ts +38 -38
- package/src/tools/mcp-client.ts +131 -131
- package/src/tools/web-scraper.ts +179 -179
- package/src/tools/web-search.ts +180 -180
- package/src/ui/components.ts +127 -127
- package/srv-out.txt +1 -1
- package/templates/ecommerce-assistant/README.md +45 -45
- package/templates/ecommerce-assistant/oad.yaml +47 -47
- package/templates/tech-support/README.md +43 -43
- package/templates/tech-support/oad.yaml +45 -45
- package/test-agent/Dockerfile +9 -9
- package/test-agent/README.md +50 -50
- package/test-agent/agent.yaml +23 -23
- package/test-agent/docker-compose.yml +11 -11
- package/test-agent/oad.yaml +31 -31
- package/test-agent/package-lock.json +1492 -1492
- package/test-agent/package.json +17 -17
- package/test-agent/src/index.ts +24 -24
- package/test-agent/src/skills/echo.ts +15 -15
- package/test-agent/tsconfig.json +24 -24
- package/test-full.js +43 -43
- package/test-sidebar.js +22 -22
- package/test-studio3.js +75 -75
- package/test-studio4.js +41 -41
- package/tests/a2a-protocol.test.ts +285 -285
- package/tests/agui-protocol.test.ts +246 -246
- package/tests/api-server.test.ts +148 -148
- package/tests/approvals.test.ts +89 -89
- package/tests/audio.test.ts +40 -40
- package/tests/brain-seed-extended.test.ts +490 -490
- package/tests/brain-seed.test.ts +239 -239
- package/tests/browser.test.ts +179 -179
- package/tests/channels/discord.test.ts +79 -79
- package/tests/channels/email.test.ts +148 -148
- package/tests/channels/feishu.test.ts +123 -123
- package/tests/channels/telegram.test.ts +129 -129
- package/tests/channels/websocket.test.ts +53 -53
- package/tests/channels/wechat.test.ts +170 -170
- package/tests/channels-extra.test.ts +45 -45
- package/tests/chat-cli.test.ts +160 -160
- package/tests/cli.test.ts +46 -46
- package/tests/context-compressor.test.ts +172 -172
- package/tests/context-refs.test.ts +121 -121
- package/tests/cron-engine.test.ts +101 -101
- package/tests/daemon.test.ts +135 -135
- package/tests/deepbrain-wire.test.ts +234 -234
- package/tests/deploy-and-dag.test.ts +196 -196
- package/tests/doctor.test.ts +38 -38
- package/tests/document-processor.test.ts +69 -69
- package/tests/e2e-nocode.test.ts +442 -442
- package/tests/elevated.test.ts +69 -69
- package/tests/eval.test.ts +173 -173
- package/tests/gateway.test.ts +63 -63
- package/tests/guardrails.test.ts +177 -177
- package/tests/home-assistant.test.ts +40 -40
- package/tests/hooks.test.ts +79 -79
- package/tests/ide-bridge.test.ts +38 -38
- package/tests/image-generator.test.ts +84 -84
- package/tests/init-role.test.ts +124 -124
- package/tests/integrations.test.ts +249 -249
- package/tests/mcp-client.test.ts +92 -92
- package/tests/mcp-server.test.ts +178 -178
- package/tests/mcp-servers.test.ts +260 -260
- package/tests/node-network.test.ts +74 -74
- package/tests/plugin-a2a-enhanced.test.ts +230 -230
- package/tests/profiles.test.ts +61 -61
- package/tests/publish.test.ts +231 -231
- package/tests/rl-tools.test.ts +93 -93
- package/tests/sandbox-manager.test.ts +46 -46
- package/tests/scheduler.test.ts +200 -200
- package/tests/secrets.test.ts +107 -107
- package/tests/security-enhanced.test.ts +233 -233
- package/tests/settings-api.test.ts +148 -148
- package/tests/setup.test.ts +73 -73
- package/tests/subagent.test.ts +193 -193
- package/tests/telegram-discord.test.ts +60 -60
- package/tests/telemetry.test.ts +186 -186
- package/tests/user-profiler.test.ts +169 -169
- package/tests/v090-features.test.ts +254 -254
- package/tests/vision.test.ts +61 -61
- package/tests/voice-call.test.ts +47 -47
- package/tests/voice-enhanced.test.ts +169 -169
- package/tests/voice-interaction.test.ts +38 -38
- package/tests/web-search.test.ts +155 -155
- package/tests/workflow-graph.test.ts +279 -279
- package/tutorial/customer-service-agent/README.md +612 -612
- package/tutorial/customer-service-agent/SOUL.md +26 -26
- package/tutorial/customer-service-agent/agent.yaml +63 -63
- package/tutorial/customer-service-agent/package.json +19 -19
- package/tutorial/customer-service-agent/src/index.ts +69 -69
- package/tutorial/customer-service-agent/src/skills/faq.ts +27 -27
- package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -22
- package/tutorial/customer-service-agent/tsconfig.json +14 -14
package/src/core/agent.ts
CHANGED
|
@@ -1,476 +1,476 @@
|
|
|
1
|
-
import { EventEmitter } from 'events';
|
|
2
|
-
import type { AgentState, IAgent, IChannel, ISkill, Message, MemoryStore, AgentContext } from './types';
|
|
3
|
-
import { InMemoryStore } from '../memory';
|
|
4
|
-
import { createProvider, type LLMProvider } from '../providers';
|
|
5
|
-
import { SkillLearner } from '../skills/auto-learn';
|
|
6
|
-
import type { MCPTool } from '../tools/mcp';
|
|
7
|
-
import { MCPToolRegistry } from '../tools/mcp';
|
|
8
|
-
import { SubAgentManager, type SubAgentConfig, type SubAgentResult } from './subagent';
|
|
9
|
-
import { Tracer } from '../telemetry';
|
|
10
|
-
import type { Span as TelemetrySpan } from '../telemetry';
|
|
11
|
-
import { BrainSeedLoader, type BrainSeedConfig } from '../memory/seed-loader';
|
|
12
|
-
import { GuardrailManager, type GuardrailConfig } from '../security/guardrails';
|
|
13
|
-
|
|
14
|
-
export class BaseAgent extends EventEmitter implements IAgent {
|
|
15
|
-
readonly name: string;
|
|
16
|
-
private _state: AgentState = 'init';
|
|
17
|
-
private skills: Map<string, ISkill> = new Map();
|
|
18
|
-
private channels: IChannel[] = [];
|
|
19
|
-
private memory: MemoryStore;
|
|
20
|
-
private _provider: LLMProvider;
|
|
21
|
-
private systemPrompt: string;
|
|
22
|
-
private historyLimit: number;
|
|
23
|
-
private toolRegistry: MCPToolRegistry = new MCPToolRegistry();
|
|
24
|
-
private maxToolRounds: number;
|
|
25
|
-
private skillLearner?: SkillLearner;
|
|
26
|
-
private autoLearnConfig: { enabled: boolean; minConversationLength: number; improveOnUse: boolean };
|
|
27
|
-
private _subAgentManager?: SubAgentManager;
|
|
28
|
-
private longTermMemory?: any;
|
|
29
|
-
private longTermMemoryConfig: { autoLearn: boolean; autoRecall: boolean } = { autoLearn: true, autoRecall: true };
|
|
30
|
-
private tracer?: Tracer;
|
|
31
|
-
private brainSeedConfig?: BrainSeedConfig;
|
|
32
|
-
private agentDir: string;
|
|
33
|
-
private guardrails?: GuardrailManager;
|
|
34
|
-
|
|
35
|
-
constructor(options: {
|
|
36
|
-
name: string;
|
|
37
|
-
systemPrompt?: string;
|
|
38
|
-
provider?: string;
|
|
39
|
-
model?: string;
|
|
40
|
-
memory?: MemoryStore;
|
|
41
|
-
historyLimit?: number;
|
|
42
|
-
skillsDir?: string;
|
|
43
|
-
learning?: {
|
|
44
|
-
autoSkillCreation?: boolean;
|
|
45
|
-
minConversationLength?: number;
|
|
46
|
-
improveOnUse?: boolean;
|
|
47
|
-
};
|
|
48
|
-
maxToolRounds?: number;
|
|
49
|
-
tracer?: Tracer;
|
|
50
|
-
agentDir?: string;
|
|
51
|
-
brainSeedConfig?: BrainSeedConfig;
|
|
52
|
-
}) {
|
|
53
|
-
super();
|
|
54
|
-
this.name = options.name;
|
|
55
|
-
this.systemPrompt = options.systemPrompt ?? 'You are a helpful AI agent.';
|
|
56
|
-
this.memory = options.memory ?? new InMemoryStore();
|
|
57
|
-
this._provider = createProvider(options.provider ?? 'openai', options.model);
|
|
58
|
-
this.historyLimit = options.historyLimit ?? 50;
|
|
59
|
-
this.maxToolRounds = options.maxToolRounds ?? 10;
|
|
60
|
-
this.autoLearnConfig = {
|
|
61
|
-
enabled: options.learning?.autoSkillCreation !== false,
|
|
62
|
-
minConversationLength: options.learning?.minConversationLength ?? 3,
|
|
63
|
-
improveOnUse: options.learning?.improveOnUse !== false,
|
|
64
|
-
};
|
|
65
|
-
if (options.skillsDir) {
|
|
66
|
-
this.skillLearner = new SkillLearner(options.skillsDir);
|
|
67
|
-
}
|
|
68
|
-
this.tracer = options.tracer;
|
|
69
|
-
this.agentDir = options.agentDir ?? process.cwd();
|
|
70
|
-
this.brainSeedConfig = options.brainSeedConfig;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
setLongTermMemory(brain: any, config?: { autoLearn?: boolean; autoRecall?: boolean }): void {
|
|
74
|
-
this.longTermMemory = brain;
|
|
75
|
-
if (config) {
|
|
76
|
-
this.longTermMemoryConfig = {
|
|
77
|
-
autoLearn: config.autoLearn !== false,
|
|
78
|
-
autoRecall: config.autoRecall !== false,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
setGuardrails(config: GuardrailConfig): void {
|
|
84
|
-
this.guardrails = new GuardrailManager(config);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
getGuardrails(): GuardrailManager | undefined {
|
|
88
|
-
return this.guardrails;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
getLongTermMemory(): any {
|
|
92
|
-
return this.longTermMemory;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
getLongTermMemoryConfig(): { autoLearn: boolean; autoRecall: boolean } {
|
|
96
|
-
return this.longTermMemoryConfig;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
get state(): AgentState {
|
|
100
|
-
return this._state;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
get provider(): LLMProvider {
|
|
104
|
-
return this._provider;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
getSystemPrompt(): string {
|
|
108
|
-
return this.systemPrompt;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
getMemory(): MemoryStore {
|
|
112
|
-
return this.memory;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
getSkillLearner(): SkillLearner | undefined {
|
|
116
|
-
return this.skillLearner;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
getToolRegistry(): MCPToolRegistry {
|
|
120
|
-
return this.toolRegistry;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
getTracer(): Tracer | undefined {
|
|
124
|
-
return this.tracer;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
setTracer(tracer: Tracer): void {
|
|
128
|
-
this.tracer = tracer;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
registerTool(tool: MCPTool): void {
|
|
132
|
-
this.toolRegistry.register(tool);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
private transition(to: AgentState): void {
|
|
136
|
-
const from = this._state;
|
|
137
|
-
this._state = to;
|
|
138
|
-
this.emit('state:change', from, to);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async init(): Promise<void> {
|
|
142
|
-
if (this.skillLearner) {
|
|
143
|
-
await this.skillLearner.loadLearnedSkills();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Auto-seed brain if configured
|
|
147
|
-
if (this.brainSeedConfig?.autoSeed && this.longTermMemory) {
|
|
148
|
-
const loader = new BrainSeedLoader(this.agentDir, this.brainSeedConfig);
|
|
149
|
-
if (!await loader.isSeeded()) {
|
|
150
|
-
const result = await loader.seedBrain(this.longTermMemory);
|
|
151
|
-
this.emit('brain:seeded', result);
|
|
152
|
-
await loader.markSeeded();
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
this.transition('ready');
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async start(): Promise<void> {
|
|
160
|
-
if (this._state !== 'ready') {
|
|
161
|
-
throw new Error(`Cannot start agent in state: ${this._state}`);
|
|
162
|
-
}
|
|
163
|
-
for (const channel of this.channels) {
|
|
164
|
-
channel.onMessage((msg) => this.handleMessage(msg));
|
|
165
|
-
await channel.start();
|
|
166
|
-
}
|
|
167
|
-
this.transition('running');
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async stop(): Promise<void> {
|
|
171
|
-
for (const channel of this.channels) {
|
|
172
|
-
await channel.stop();
|
|
173
|
-
}
|
|
174
|
-
this.transition('stopped');
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
registerSkill(skill: ISkill): void {
|
|
178
|
-
this.skills.set(skill.name, skill);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
bindChannel(channel: IChannel): void {
|
|
182
|
-
this.channels.push(channel);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
getChannels(): IChannel[] {
|
|
186
|
-
return this.channels;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
private getSubAgentManager(): SubAgentManager {
|
|
190
|
-
if (!this._subAgentManager) {
|
|
191
|
-
this._subAgentManager = new SubAgentManager();
|
|
192
|
-
}
|
|
193
|
-
return this._subAgentManager;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async spawnSubAgent(config: SubAgentConfig): Promise<SubAgentResult> {
|
|
197
|
-
return this.getSubAgentManager().spawn(config, this._provider);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async spawnParallel(configs: SubAgentConfig[]): Promise<SubAgentResult[]> {
|
|
201
|
-
return this.getSubAgentManager().spawnParallel(configs, this._provider);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
async handleMessage(message: Message): Promise<Message> {
|
|
205
|
-
this.emit('message:in', message);
|
|
206
|
-
|
|
207
|
-
// Start root span if tracer is configured
|
|
208
|
-
let rootSpan: TelemetrySpan | undefined;
|
|
209
|
-
if (this.tracer) {
|
|
210
|
-
rootSpan = this.tracer.startSpan('handleMessage', {
|
|
211
|
-
kind: 'server',
|
|
212
|
-
attributes: {
|
|
213
|
-
'message.channel': (message.metadata?.channel as string) || 'unknown',
|
|
214
|
-
'message.sender': (message.metadata?.sender as string) || 'unknown',
|
|
215
|
-
'message.length': message.content.length,
|
|
216
|
-
},
|
|
217
|
-
});
|
|
218
|
-
this.tracer.increment('agent.messages.total', 1, { agent: this.name });
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const sessionId = (message.metadata?.sessionId as string) ?? 'default';
|
|
222
|
-
await this.memory.addMessage(sessionId, message);
|
|
223
|
-
|
|
224
|
-
// === Guardrails: check input ===
|
|
225
|
-
if (this.guardrails) {
|
|
226
|
-
const inputCheck = await this.guardrails.checkInput(message.content);
|
|
227
|
-
if (inputCheck.blocked) {
|
|
228
|
-
const blockedResponse = this.createResponse(inputCheck.message ?? 'Message blocked by guardrails.', message);
|
|
229
|
-
await this.memory.addMessage(sessionId, blockedResponse);
|
|
230
|
-
this.emit('message:out', blockedResponse);
|
|
231
|
-
if (rootSpan && this.tracer) {
|
|
232
|
-
this.tracer.addEvent(rootSpan, 'guardrail.blocked', { rule: inputCheck.violations[0]?.rule ?? 'unknown' });
|
|
233
|
-
this.tracer.endSpan(rootSpan, 'ok');
|
|
234
|
-
}
|
|
235
|
-
return blockedResponse;
|
|
236
|
-
}
|
|
237
|
-
if (inputCheck.redacted && inputCheck.redactedText) {
|
|
238
|
-
message = { ...message, content: inputCheck.redactedText };
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// === Recall from long-term memory ===
|
|
243
|
-
let memoryContext = '';
|
|
244
|
-
if (this.longTermMemory && this.longTermMemoryConfig.autoRecall) {
|
|
245
|
-
let memorySpan: TelemetrySpan | undefined;
|
|
246
|
-
if (this.tracer && rootSpan) {
|
|
247
|
-
memorySpan = this.tracer.startSpan('memory.recall', { parent: rootSpan, kind: 'client' });
|
|
248
|
-
}
|
|
249
|
-
try {
|
|
250
|
-
const recalled = await this.longTermMemory.recall(message.content);
|
|
251
|
-
if (recalled && (Array.isArray(recalled) ? recalled.length > 0 : true)) {
|
|
252
|
-
memoryContext = '\n\n[Relevant memories]\n' +
|
|
253
|
-
(Array.isArray(recalled)
|
|
254
|
-
? recalled.map((r: any) => typeof r === 'string' ? r : r.content || r.compiled_truth || '').join('\n')
|
|
255
|
-
: String(recalled));
|
|
256
|
-
}
|
|
257
|
-
if (this.tracer && memorySpan) this.tracer.endSpan(memorySpan, 'ok');
|
|
258
|
-
} catch {
|
|
259
|
-
if (this.tracer && memorySpan) this.tracer.endSpan(memorySpan, 'error');
|
|
260
|
-
// Silent fail — don't break chat if memory fails
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const context: AgentContext = {
|
|
265
|
-
agentName: this.name,
|
|
266
|
-
sessionId,
|
|
267
|
-
messages: (await this.memory.getConversation(sessionId)).slice(-this.historyLimit),
|
|
268
|
-
memory: this.memory,
|
|
269
|
-
metadata: {},
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
// Try skills first
|
|
273
|
-
for (const [name, skill] of this.skills) {
|
|
274
|
-
try {
|
|
275
|
-
const result = await skill.execute(context, message);
|
|
276
|
-
this.emit('skill:execute', name, result);
|
|
277
|
-
if (result.handled && result.response) {
|
|
278
|
-
const response = this.createResponse(result.response, message);
|
|
279
|
-
await this.memory.addMessage(sessionId, response);
|
|
280
|
-
this.emit('message:out', response);
|
|
281
|
-
return response;
|
|
282
|
-
}
|
|
283
|
-
} catch (err) {
|
|
284
|
-
this.emit('error', err instanceof Error ? err : new Error(String(err)));
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Check if a learned skill matches — prepend instructions to system prompt
|
|
289
|
-
let effectiveSystemPrompt = this.systemPrompt;
|
|
290
|
-
|
|
291
|
-
// Inject long-term memory context
|
|
292
|
-
if (memoryContext) {
|
|
293
|
-
effectiveSystemPrompt = effectiveSystemPrompt + memoryContext;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const matchedSkill = this.skillLearner?.matchSkill(message.content);
|
|
297
|
-
if (matchedSkill) {
|
|
298
|
-
matchedSkill.usageCount++;
|
|
299
|
-
matchedSkill.lastUsed = new Date();
|
|
300
|
-
effectiveSystemPrompt = `[Learned Skill: ${matchedSkill.name}]\n${matchedSkill.instructions}\n\n${this.systemPrompt}`;
|
|
301
|
-
this.emit('skill:matched', matchedSkill);
|
|
302
|
-
if (this.tracer && rootSpan) {
|
|
303
|
-
this.tracer.addEvent(rootSpan, 'skill.matched', { 'skill.name': matchedSkill.name });
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Fall back to LLM with tool use loop
|
|
308
|
-
const tools = this.toolRegistry.list();
|
|
309
|
-
const llmMessages = [...context.messages];
|
|
310
|
-
let finalResponse = '';
|
|
311
|
-
|
|
312
|
-
for (let round = 0; round <= this.maxToolRounds; round++) {
|
|
313
|
-
let llmSpan: TelemetrySpan | undefined;
|
|
314
|
-
if (this.tracer && rootSpan) {
|
|
315
|
-
llmSpan = this.tracer.startSpan('llm.chat', {
|
|
316
|
-
parent: rootSpan,
|
|
317
|
-
kind: 'client',
|
|
318
|
-
attributes: { 'llm.round': round },
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const llmResponse = await this._provider.chat(
|
|
323
|
-
llmMessages,
|
|
324
|
-
effectiveSystemPrompt,
|
|
325
|
-
{ tools: tools.length > 0 ? tools : undefined },
|
|
326
|
-
);
|
|
327
|
-
|
|
328
|
-
if (this.tracer && llmSpan) {
|
|
329
|
-
llmSpan.attributes['llm.response.length'] = llmResponse.length;
|
|
330
|
-
this.tracer.endSpan(llmSpan, 'ok');
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const toolCall = this.parseToolCall(llmResponse);
|
|
334
|
-
if (!toolCall || tools.length === 0 || round === this.maxToolRounds) {
|
|
335
|
-
finalResponse = llmResponse;
|
|
336
|
-
break;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Execute tool
|
|
340
|
-
let toolSpan: TelemetrySpan | undefined;
|
|
341
|
-
if (this.tracer && rootSpan) {
|
|
342
|
-
toolSpan = this.tracer.startSpan('tool.execute', {
|
|
343
|
-
parent: rootSpan,
|
|
344
|
-
kind: 'internal',
|
|
345
|
-
attributes: { 'tool.name': toolCall.name },
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const toolResult = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
|
|
350
|
-
this.emit('tool:execute', toolCall.name, toolResult);
|
|
351
|
-
|
|
352
|
-
if (this.tracer && toolSpan) {
|
|
353
|
-
toolSpan.attributes['tool.result.length'] = toolResult.content?.length || 0;
|
|
354
|
-
this.tracer.endSpan(toolSpan, 'ok');
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Add tool call and result to messages for next round
|
|
358
|
-
llmMessages.push({
|
|
359
|
-
id: `tool_call_${Date.now()}`,
|
|
360
|
-
role: 'assistant',
|
|
361
|
-
content: llmResponse,
|
|
362
|
-
timestamp: Date.now(),
|
|
363
|
-
});
|
|
364
|
-
llmMessages.push({
|
|
365
|
-
id: `tool_result_${Date.now()}`,
|
|
366
|
-
role: 'user',
|
|
367
|
-
content: `[Tool Result for ${toolCall.name}]: ${toolResult.content}`,
|
|
368
|
-
timestamp: Date.now(),
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// === Guardrails: check output ===
|
|
373
|
-
if (this.guardrails) {
|
|
374
|
-
const outputCheck = await this.guardrails.checkOutput(finalResponse);
|
|
375
|
-
if (outputCheck.blocked) {
|
|
376
|
-
finalResponse = outputCheck.message ?? 'Response blocked by guardrails.';
|
|
377
|
-
} else if (outputCheck.redacted && outputCheck.redactedText) {
|
|
378
|
-
finalResponse = outputCheck.redactedText;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
const response = this.createResponse(finalResponse, message);
|
|
383
|
-
await this.memory.addMessage(sessionId, response);
|
|
384
|
-
this.emit('message:out', response);
|
|
385
|
-
|
|
386
|
-
// End root telemetry span
|
|
387
|
-
if (this.tracer && rootSpan) {
|
|
388
|
-
rootSpan.attributes['response.length'] = finalResponse.length;
|
|
389
|
-
this.tracer.endSpan(rootSpan, 'ok');
|
|
390
|
-
this.tracer.histogram('agent.message.duration', rootSpan.endTime! - rootSpan.startTime, { agent: this.name });
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// === Learn from interaction ===
|
|
394
|
-
if (this.longTermMemory && this.longTermMemoryConfig.autoLearn) {
|
|
395
|
-
try {
|
|
396
|
-
await this.longTermMemory.learn(
|
|
397
|
-
`User: ${message.content}\nAssistant: ${finalResponse}`,
|
|
398
|
-
{ tags: ['conversation', (message.metadata?.channel as string) || 'unknown'] },
|
|
399
|
-
);
|
|
400
|
-
} catch {
|
|
401
|
-
// Silent fail
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// After response, check if we should learn a skill
|
|
406
|
-
if (
|
|
407
|
-
this.skillLearner &&
|
|
408
|
-
this.autoLearnConfig.enabled &&
|
|
409
|
-
context.messages.length >= this.autoLearnConfig.minConversationLength
|
|
410
|
-
) {
|
|
411
|
-
this.skillLearner
|
|
412
|
-
.analyzeForSkillCreation(context.messages, this._provider)
|
|
413
|
-
.then(async (learnedSkill) => {
|
|
414
|
-
if (learnedSkill) {
|
|
415
|
-
await this.skillLearner!.saveSkill(learnedSkill);
|
|
416
|
-
this.emit('skill:learned', learnedSkill);
|
|
417
|
-
}
|
|
418
|
-
})
|
|
419
|
-
.catch(() => {});
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Improve matched skill after use
|
|
423
|
-
if (matchedSkill && this.skillLearner && this.autoLearnConfig.improveOnUse) {
|
|
424
|
-
this.skillLearner
|
|
425
|
-
.improveSkill(matchedSkill, context.messages, this._provider)
|
|
426
|
-
.then(() => this.skillLearner!.saveSkill(matchedSkill))
|
|
427
|
-
.catch(() => {});
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
return response;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
private parseToolCall(response: string): { name: string; arguments: Record<string, unknown> } | null {
|
|
434
|
-
try {
|
|
435
|
-
const parsed = JSON.parse(response);
|
|
436
|
-
if (parsed.tool_call) return parsed.tool_call;
|
|
437
|
-
if (parsed.name && parsed.arguments !== undefined) return parsed;
|
|
438
|
-
} catch { /* not JSON */ }
|
|
439
|
-
|
|
440
|
-
const match = response.match(/<tool_call>\s*(\{[\s\S]*?\})\s*<\/tool_call>/);
|
|
441
|
-
if (match) {
|
|
442
|
-
try {
|
|
443
|
-
const parsed = JSON.parse(match[1]);
|
|
444
|
-
if (parsed.name) return parsed;
|
|
445
|
-
} catch { /* not valid JSON */ }
|
|
446
|
-
}
|
|
447
|
-
return null;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
async *handleMessageStream(message: Message): AsyncIterable<string> {
|
|
451
|
-
const sessionId = (message.metadata?.sessionId as string) ?? 'default';
|
|
452
|
-
await this.memory.addMessage(sessionId, message);
|
|
453
|
-
|
|
454
|
-
const history = (await this.memory.getConversation(sessionId)).slice(-this.historyLimit);
|
|
455
|
-
|
|
456
|
-
let fullResponse = '';
|
|
457
|
-
for await (const chunk of this._provider.chatStream(history, this.systemPrompt)) {
|
|
458
|
-
fullResponse += chunk;
|
|
459
|
-
yield chunk;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
const response = this.createResponse(fullResponse, message);
|
|
463
|
-
await this.memory.addMessage(sessionId, response);
|
|
464
|
-
this.emit('message:out', response);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
private createResponse(content: string, inReplyTo: Message): Message {
|
|
468
|
-
return {
|
|
469
|
-
id: `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
470
|
-
role: 'assistant',
|
|
471
|
-
content,
|
|
472
|
-
timestamp: Date.now(),
|
|
473
|
-
metadata: { inReplyTo: inReplyTo.id },
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
}
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import type { AgentState, IAgent, IChannel, ISkill, Message, MemoryStore, AgentContext } from './types';
|
|
3
|
+
import { InMemoryStore } from '../memory';
|
|
4
|
+
import { createProvider, type LLMProvider } from '../providers';
|
|
5
|
+
import { SkillLearner } from '../skills/auto-learn';
|
|
6
|
+
import type { MCPTool } from '../tools/mcp';
|
|
7
|
+
import { MCPToolRegistry } from '../tools/mcp';
|
|
8
|
+
import { SubAgentManager, type SubAgentConfig, type SubAgentResult } from './subagent';
|
|
9
|
+
import { Tracer } from '../telemetry';
|
|
10
|
+
import type { Span as TelemetrySpan } from '../telemetry';
|
|
11
|
+
import { BrainSeedLoader, type BrainSeedConfig } from '../memory/seed-loader';
|
|
12
|
+
import { GuardrailManager, type GuardrailConfig } from '../security/guardrails';
|
|
13
|
+
|
|
14
|
+
export class BaseAgent extends EventEmitter implements IAgent {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
private _state: AgentState = 'init';
|
|
17
|
+
private skills: Map<string, ISkill> = new Map();
|
|
18
|
+
private channels: IChannel[] = [];
|
|
19
|
+
private memory: MemoryStore;
|
|
20
|
+
private _provider: LLMProvider;
|
|
21
|
+
private systemPrompt: string;
|
|
22
|
+
private historyLimit: number;
|
|
23
|
+
private toolRegistry: MCPToolRegistry = new MCPToolRegistry();
|
|
24
|
+
private maxToolRounds: number;
|
|
25
|
+
private skillLearner?: SkillLearner;
|
|
26
|
+
private autoLearnConfig: { enabled: boolean; minConversationLength: number; improveOnUse: boolean };
|
|
27
|
+
private _subAgentManager?: SubAgentManager;
|
|
28
|
+
private longTermMemory?: any;
|
|
29
|
+
private longTermMemoryConfig: { autoLearn: boolean; autoRecall: boolean } = { autoLearn: true, autoRecall: true };
|
|
30
|
+
private tracer?: Tracer;
|
|
31
|
+
private brainSeedConfig?: BrainSeedConfig;
|
|
32
|
+
private agentDir: string;
|
|
33
|
+
private guardrails?: GuardrailManager;
|
|
34
|
+
|
|
35
|
+
constructor(options: {
|
|
36
|
+
name: string;
|
|
37
|
+
systemPrompt?: string;
|
|
38
|
+
provider?: string;
|
|
39
|
+
model?: string;
|
|
40
|
+
memory?: MemoryStore;
|
|
41
|
+
historyLimit?: number;
|
|
42
|
+
skillsDir?: string;
|
|
43
|
+
learning?: {
|
|
44
|
+
autoSkillCreation?: boolean;
|
|
45
|
+
minConversationLength?: number;
|
|
46
|
+
improveOnUse?: boolean;
|
|
47
|
+
};
|
|
48
|
+
maxToolRounds?: number;
|
|
49
|
+
tracer?: Tracer;
|
|
50
|
+
agentDir?: string;
|
|
51
|
+
brainSeedConfig?: BrainSeedConfig;
|
|
52
|
+
}) {
|
|
53
|
+
super();
|
|
54
|
+
this.name = options.name;
|
|
55
|
+
this.systemPrompt = options.systemPrompt ?? 'You are a helpful AI agent.';
|
|
56
|
+
this.memory = options.memory ?? new InMemoryStore();
|
|
57
|
+
this._provider = createProvider(options.provider ?? 'openai', options.model);
|
|
58
|
+
this.historyLimit = options.historyLimit ?? 50;
|
|
59
|
+
this.maxToolRounds = options.maxToolRounds ?? 10;
|
|
60
|
+
this.autoLearnConfig = {
|
|
61
|
+
enabled: options.learning?.autoSkillCreation !== false,
|
|
62
|
+
minConversationLength: options.learning?.minConversationLength ?? 3,
|
|
63
|
+
improveOnUse: options.learning?.improveOnUse !== false,
|
|
64
|
+
};
|
|
65
|
+
if (options.skillsDir) {
|
|
66
|
+
this.skillLearner = new SkillLearner(options.skillsDir);
|
|
67
|
+
}
|
|
68
|
+
this.tracer = options.tracer;
|
|
69
|
+
this.agentDir = options.agentDir ?? process.cwd();
|
|
70
|
+
this.brainSeedConfig = options.brainSeedConfig;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
setLongTermMemory(brain: any, config?: { autoLearn?: boolean; autoRecall?: boolean }): void {
|
|
74
|
+
this.longTermMemory = brain;
|
|
75
|
+
if (config) {
|
|
76
|
+
this.longTermMemoryConfig = {
|
|
77
|
+
autoLearn: config.autoLearn !== false,
|
|
78
|
+
autoRecall: config.autoRecall !== false,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
setGuardrails(config: GuardrailConfig): void {
|
|
84
|
+
this.guardrails = new GuardrailManager(config);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getGuardrails(): GuardrailManager | undefined {
|
|
88
|
+
return this.guardrails;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getLongTermMemory(): any {
|
|
92
|
+
return this.longTermMemory;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
getLongTermMemoryConfig(): { autoLearn: boolean; autoRecall: boolean } {
|
|
96
|
+
return this.longTermMemoryConfig;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
get state(): AgentState {
|
|
100
|
+
return this._state;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get provider(): LLMProvider {
|
|
104
|
+
return this._provider;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getSystemPrompt(): string {
|
|
108
|
+
return this.systemPrompt;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
getMemory(): MemoryStore {
|
|
112
|
+
return this.memory;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
getSkillLearner(): SkillLearner | undefined {
|
|
116
|
+
return this.skillLearner;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
getToolRegistry(): MCPToolRegistry {
|
|
120
|
+
return this.toolRegistry;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getTracer(): Tracer | undefined {
|
|
124
|
+
return this.tracer;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
setTracer(tracer: Tracer): void {
|
|
128
|
+
this.tracer = tracer;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
registerTool(tool: MCPTool): void {
|
|
132
|
+
this.toolRegistry.register(tool);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private transition(to: AgentState): void {
|
|
136
|
+
const from = this._state;
|
|
137
|
+
this._state = to;
|
|
138
|
+
this.emit('state:change', from, to);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async init(): Promise<void> {
|
|
142
|
+
if (this.skillLearner) {
|
|
143
|
+
await this.skillLearner.loadLearnedSkills();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Auto-seed brain if configured
|
|
147
|
+
if (this.brainSeedConfig?.autoSeed && this.longTermMemory) {
|
|
148
|
+
const loader = new BrainSeedLoader(this.agentDir, this.brainSeedConfig);
|
|
149
|
+
if (!await loader.isSeeded()) {
|
|
150
|
+
const result = await loader.seedBrain(this.longTermMemory);
|
|
151
|
+
this.emit('brain:seeded', result);
|
|
152
|
+
await loader.markSeeded();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this.transition('ready');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async start(): Promise<void> {
|
|
160
|
+
if (this._state !== 'ready') {
|
|
161
|
+
throw new Error(`Cannot start agent in state: ${this._state}`);
|
|
162
|
+
}
|
|
163
|
+
for (const channel of this.channels) {
|
|
164
|
+
channel.onMessage((msg) => this.handleMessage(msg));
|
|
165
|
+
await channel.start();
|
|
166
|
+
}
|
|
167
|
+
this.transition('running');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async stop(): Promise<void> {
|
|
171
|
+
for (const channel of this.channels) {
|
|
172
|
+
await channel.stop();
|
|
173
|
+
}
|
|
174
|
+
this.transition('stopped');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
registerSkill(skill: ISkill): void {
|
|
178
|
+
this.skills.set(skill.name, skill);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
bindChannel(channel: IChannel): void {
|
|
182
|
+
this.channels.push(channel);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
getChannels(): IChannel[] {
|
|
186
|
+
return this.channels;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private getSubAgentManager(): SubAgentManager {
|
|
190
|
+
if (!this._subAgentManager) {
|
|
191
|
+
this._subAgentManager = new SubAgentManager();
|
|
192
|
+
}
|
|
193
|
+
return this._subAgentManager;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async spawnSubAgent(config: SubAgentConfig): Promise<SubAgentResult> {
|
|
197
|
+
return this.getSubAgentManager().spawn(config, this._provider);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async spawnParallel(configs: SubAgentConfig[]): Promise<SubAgentResult[]> {
|
|
201
|
+
return this.getSubAgentManager().spawnParallel(configs, this._provider);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async handleMessage(message: Message): Promise<Message> {
|
|
205
|
+
this.emit('message:in', message);
|
|
206
|
+
|
|
207
|
+
// Start root span if tracer is configured
|
|
208
|
+
let rootSpan: TelemetrySpan | undefined;
|
|
209
|
+
if (this.tracer) {
|
|
210
|
+
rootSpan = this.tracer.startSpan('handleMessage', {
|
|
211
|
+
kind: 'server',
|
|
212
|
+
attributes: {
|
|
213
|
+
'message.channel': (message.metadata?.channel as string) || 'unknown',
|
|
214
|
+
'message.sender': (message.metadata?.sender as string) || 'unknown',
|
|
215
|
+
'message.length': message.content.length,
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
this.tracer.increment('agent.messages.total', 1, { agent: this.name });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const sessionId = (message.metadata?.sessionId as string) ?? 'default';
|
|
222
|
+
await this.memory.addMessage(sessionId, message);
|
|
223
|
+
|
|
224
|
+
// === Guardrails: check input ===
|
|
225
|
+
if (this.guardrails) {
|
|
226
|
+
const inputCheck = await this.guardrails.checkInput(message.content);
|
|
227
|
+
if (inputCheck.blocked) {
|
|
228
|
+
const blockedResponse = this.createResponse(inputCheck.message ?? 'Message blocked by guardrails.', message);
|
|
229
|
+
await this.memory.addMessage(sessionId, blockedResponse);
|
|
230
|
+
this.emit('message:out', blockedResponse);
|
|
231
|
+
if (rootSpan && this.tracer) {
|
|
232
|
+
this.tracer.addEvent(rootSpan, 'guardrail.blocked', { rule: inputCheck.violations[0]?.rule ?? 'unknown' });
|
|
233
|
+
this.tracer.endSpan(rootSpan, 'ok');
|
|
234
|
+
}
|
|
235
|
+
return blockedResponse;
|
|
236
|
+
}
|
|
237
|
+
if (inputCheck.redacted && inputCheck.redactedText) {
|
|
238
|
+
message = { ...message, content: inputCheck.redactedText };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// === Recall from long-term memory ===
|
|
243
|
+
let memoryContext = '';
|
|
244
|
+
if (this.longTermMemory && this.longTermMemoryConfig.autoRecall) {
|
|
245
|
+
let memorySpan: TelemetrySpan | undefined;
|
|
246
|
+
if (this.tracer && rootSpan) {
|
|
247
|
+
memorySpan = this.tracer.startSpan('memory.recall', { parent: rootSpan, kind: 'client' });
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const recalled = await this.longTermMemory.recall(message.content);
|
|
251
|
+
if (recalled && (Array.isArray(recalled) ? recalled.length > 0 : true)) {
|
|
252
|
+
memoryContext = '\n\n[Relevant memories]\n' +
|
|
253
|
+
(Array.isArray(recalled)
|
|
254
|
+
? recalled.map((r: any) => typeof r === 'string' ? r : r.content || r.compiled_truth || '').join('\n')
|
|
255
|
+
: String(recalled));
|
|
256
|
+
}
|
|
257
|
+
if (this.tracer && memorySpan) this.tracer.endSpan(memorySpan, 'ok');
|
|
258
|
+
} catch {
|
|
259
|
+
if (this.tracer && memorySpan) this.tracer.endSpan(memorySpan, 'error');
|
|
260
|
+
// Silent fail — don't break chat if memory fails
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const context: AgentContext = {
|
|
265
|
+
agentName: this.name,
|
|
266
|
+
sessionId,
|
|
267
|
+
messages: (await this.memory.getConversation(sessionId)).slice(-this.historyLimit),
|
|
268
|
+
memory: this.memory,
|
|
269
|
+
metadata: {},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Try skills first
|
|
273
|
+
for (const [name, skill] of this.skills) {
|
|
274
|
+
try {
|
|
275
|
+
const result = await skill.execute(context, message);
|
|
276
|
+
this.emit('skill:execute', name, result);
|
|
277
|
+
if (result.handled && result.response) {
|
|
278
|
+
const response = this.createResponse(result.response, message);
|
|
279
|
+
await this.memory.addMessage(sessionId, response);
|
|
280
|
+
this.emit('message:out', response);
|
|
281
|
+
return response;
|
|
282
|
+
}
|
|
283
|
+
} catch (err) {
|
|
284
|
+
this.emit('error', err instanceof Error ? err : new Error(String(err)));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Check if a learned skill matches — prepend instructions to system prompt
|
|
289
|
+
let effectiveSystemPrompt = this.systemPrompt;
|
|
290
|
+
|
|
291
|
+
// Inject long-term memory context
|
|
292
|
+
if (memoryContext) {
|
|
293
|
+
effectiveSystemPrompt = effectiveSystemPrompt + memoryContext;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const matchedSkill = this.skillLearner?.matchSkill(message.content);
|
|
297
|
+
if (matchedSkill) {
|
|
298
|
+
matchedSkill.usageCount++;
|
|
299
|
+
matchedSkill.lastUsed = new Date();
|
|
300
|
+
effectiveSystemPrompt = `[Learned Skill: ${matchedSkill.name}]\n${matchedSkill.instructions}\n\n${this.systemPrompt}`;
|
|
301
|
+
this.emit('skill:matched', matchedSkill);
|
|
302
|
+
if (this.tracer && rootSpan) {
|
|
303
|
+
this.tracer.addEvent(rootSpan, 'skill.matched', { 'skill.name': matchedSkill.name });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Fall back to LLM with tool use loop
|
|
308
|
+
const tools = this.toolRegistry.list();
|
|
309
|
+
const llmMessages = [...context.messages];
|
|
310
|
+
let finalResponse = '';
|
|
311
|
+
|
|
312
|
+
for (let round = 0; round <= this.maxToolRounds; round++) {
|
|
313
|
+
let llmSpan: TelemetrySpan | undefined;
|
|
314
|
+
if (this.tracer && rootSpan) {
|
|
315
|
+
llmSpan = this.tracer.startSpan('llm.chat', {
|
|
316
|
+
parent: rootSpan,
|
|
317
|
+
kind: 'client',
|
|
318
|
+
attributes: { 'llm.round': round },
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const llmResponse = await this._provider.chat(
|
|
323
|
+
llmMessages,
|
|
324
|
+
effectiveSystemPrompt,
|
|
325
|
+
{ tools: tools.length > 0 ? tools : undefined },
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
if (this.tracer && llmSpan) {
|
|
329
|
+
llmSpan.attributes['llm.response.length'] = llmResponse.length;
|
|
330
|
+
this.tracer.endSpan(llmSpan, 'ok');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const toolCall = this.parseToolCall(llmResponse);
|
|
334
|
+
if (!toolCall || tools.length === 0 || round === this.maxToolRounds) {
|
|
335
|
+
finalResponse = llmResponse;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Execute tool
|
|
340
|
+
let toolSpan: TelemetrySpan | undefined;
|
|
341
|
+
if (this.tracer && rootSpan) {
|
|
342
|
+
toolSpan = this.tracer.startSpan('tool.execute', {
|
|
343
|
+
parent: rootSpan,
|
|
344
|
+
kind: 'internal',
|
|
345
|
+
attributes: { 'tool.name': toolCall.name },
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const toolResult = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
|
|
350
|
+
this.emit('tool:execute', toolCall.name, toolResult);
|
|
351
|
+
|
|
352
|
+
if (this.tracer && toolSpan) {
|
|
353
|
+
toolSpan.attributes['tool.result.length'] = toolResult.content?.length || 0;
|
|
354
|
+
this.tracer.endSpan(toolSpan, 'ok');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Add tool call and result to messages for next round
|
|
358
|
+
llmMessages.push({
|
|
359
|
+
id: `tool_call_${Date.now()}`,
|
|
360
|
+
role: 'assistant',
|
|
361
|
+
content: llmResponse,
|
|
362
|
+
timestamp: Date.now(),
|
|
363
|
+
});
|
|
364
|
+
llmMessages.push({
|
|
365
|
+
id: `tool_result_${Date.now()}`,
|
|
366
|
+
role: 'user',
|
|
367
|
+
content: `[Tool Result for ${toolCall.name}]: ${toolResult.content}`,
|
|
368
|
+
timestamp: Date.now(),
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// === Guardrails: check output ===
|
|
373
|
+
if (this.guardrails) {
|
|
374
|
+
const outputCheck = await this.guardrails.checkOutput(finalResponse);
|
|
375
|
+
if (outputCheck.blocked) {
|
|
376
|
+
finalResponse = outputCheck.message ?? 'Response blocked by guardrails.';
|
|
377
|
+
} else if (outputCheck.redacted && outputCheck.redactedText) {
|
|
378
|
+
finalResponse = outputCheck.redactedText;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const response = this.createResponse(finalResponse, message);
|
|
383
|
+
await this.memory.addMessage(sessionId, response);
|
|
384
|
+
this.emit('message:out', response);
|
|
385
|
+
|
|
386
|
+
// End root telemetry span
|
|
387
|
+
if (this.tracer && rootSpan) {
|
|
388
|
+
rootSpan.attributes['response.length'] = finalResponse.length;
|
|
389
|
+
this.tracer.endSpan(rootSpan, 'ok');
|
|
390
|
+
this.tracer.histogram('agent.message.duration', rootSpan.endTime! - rootSpan.startTime, { agent: this.name });
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// === Learn from interaction ===
|
|
394
|
+
if (this.longTermMemory && this.longTermMemoryConfig.autoLearn) {
|
|
395
|
+
try {
|
|
396
|
+
await this.longTermMemory.learn(
|
|
397
|
+
`User: ${message.content}\nAssistant: ${finalResponse}`,
|
|
398
|
+
{ tags: ['conversation', (message.metadata?.channel as string) || 'unknown'] },
|
|
399
|
+
);
|
|
400
|
+
} catch {
|
|
401
|
+
// Silent fail
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// After response, check if we should learn a skill
|
|
406
|
+
if (
|
|
407
|
+
this.skillLearner &&
|
|
408
|
+
this.autoLearnConfig.enabled &&
|
|
409
|
+
context.messages.length >= this.autoLearnConfig.minConversationLength
|
|
410
|
+
) {
|
|
411
|
+
this.skillLearner
|
|
412
|
+
.analyzeForSkillCreation(context.messages, this._provider)
|
|
413
|
+
.then(async (learnedSkill) => {
|
|
414
|
+
if (learnedSkill) {
|
|
415
|
+
await this.skillLearner!.saveSkill(learnedSkill);
|
|
416
|
+
this.emit('skill:learned', learnedSkill);
|
|
417
|
+
}
|
|
418
|
+
})
|
|
419
|
+
.catch(() => {});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Improve matched skill after use
|
|
423
|
+
if (matchedSkill && this.skillLearner && this.autoLearnConfig.improveOnUse) {
|
|
424
|
+
this.skillLearner
|
|
425
|
+
.improveSkill(matchedSkill, context.messages, this._provider)
|
|
426
|
+
.then(() => this.skillLearner!.saveSkill(matchedSkill))
|
|
427
|
+
.catch(() => {});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return response;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private parseToolCall(response: string): { name: string; arguments: Record<string, unknown> } | null {
|
|
434
|
+
try {
|
|
435
|
+
const parsed = JSON.parse(response);
|
|
436
|
+
if (parsed.tool_call) return parsed.tool_call;
|
|
437
|
+
if (parsed.name && parsed.arguments !== undefined) return parsed;
|
|
438
|
+
} catch { /* not JSON */ }
|
|
439
|
+
|
|
440
|
+
const match = response.match(/<tool_call>\s*(\{[\s\S]*?\})\s*<\/tool_call>/);
|
|
441
|
+
if (match) {
|
|
442
|
+
try {
|
|
443
|
+
const parsed = JSON.parse(match[1]);
|
|
444
|
+
if (parsed.name) return parsed;
|
|
445
|
+
} catch { /* not valid JSON */ }
|
|
446
|
+
}
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async *handleMessageStream(message: Message): AsyncIterable<string> {
|
|
451
|
+
const sessionId = (message.metadata?.sessionId as string) ?? 'default';
|
|
452
|
+
await this.memory.addMessage(sessionId, message);
|
|
453
|
+
|
|
454
|
+
const history = (await this.memory.getConversation(sessionId)).slice(-this.historyLimit);
|
|
455
|
+
|
|
456
|
+
let fullResponse = '';
|
|
457
|
+
for await (const chunk of this._provider.chatStream(history, this.systemPrompt)) {
|
|
458
|
+
fullResponse += chunk;
|
|
459
|
+
yield chunk;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const response = this.createResponse(fullResponse, message);
|
|
463
|
+
await this.memory.addMessage(sessionId, response);
|
|
464
|
+
this.emit('message:out', response);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private createResponse(content: string, inReplyTo: Message): Message {
|
|
468
|
+
return {
|
|
469
|
+
id: `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
470
|
+
role: 'assistant',
|
|
471
|
+
content,
|
|
472
|
+
timestamp: Date.now(),
|
|
473
|
+
metadata: { inReplyTo: inReplyTo.id },
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
}
|