opc-agent 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +91 -32
  3. package/dist/channels/telegram.d.ts +30 -9
  4. package/dist/channels/telegram.js +125 -33
  5. package/dist/cli.js +415 -8
  6. package/dist/core/agent.d.ts +23 -0
  7. package/dist/core/agent.js +120 -3
  8. package/dist/core/runtime.d.ts +1 -0
  9. package/dist/core/runtime.js +44 -0
  10. package/dist/core/scheduler.d.ts +52 -0
  11. package/dist/core/scheduler.js +168 -0
  12. package/dist/core/subagent.d.ts +28 -0
  13. package/dist/core/subagent.js +65 -0
  14. package/dist/daemon.d.ts +3 -0
  15. package/dist/daemon.js +134 -0
  16. package/dist/index.d.ts +7 -0
  17. package/dist/index.js +17 -1
  18. package/dist/providers/index.d.ts +5 -1
  19. package/dist/providers/index.js +16 -9
  20. package/dist/schema/oad.d.ts +179 -4
  21. package/dist/schema/oad.js +12 -1
  22. package/dist/skills/auto-learn.d.ts +28 -0
  23. package/dist/skills/auto-learn.js +257 -0
  24. package/dist/tools/builtin/datetime.d.ts +3 -0
  25. package/dist/tools/builtin/datetime.js +44 -0
  26. package/dist/tools/builtin/file.d.ts +3 -0
  27. package/dist/tools/builtin/file.js +151 -0
  28. package/dist/tools/builtin/index.d.ts +15 -0
  29. package/dist/tools/builtin/index.js +30 -0
  30. package/dist/tools/builtin/shell.d.ts +3 -0
  31. package/dist/tools/builtin/shell.js +43 -0
  32. package/dist/tools/builtin/web.d.ts +3 -0
  33. package/dist/tools/builtin/web.js +37 -0
  34. package/dist/tools/mcp-client.d.ts +24 -0
  35. package/dist/tools/mcp-client.js +119 -0
  36. package/package.json +1 -1
  37. package/src/channels/telegram.ts +212 -90
  38. package/src/cli.ts +418 -8
  39. package/src/core/agent.ts +295 -152
  40. package/src/core/runtime.ts +47 -0
  41. package/src/core/scheduler.ts +187 -0
  42. package/src/core/subagent.ts +98 -0
  43. package/src/daemon.ts +96 -0
  44. package/src/index.ts +11 -0
  45. package/src/providers/index.ts +354 -339
  46. package/src/schema/oad.ts +167 -154
  47. package/src/skills/auto-learn.ts +262 -0
  48. package/src/tools/builtin/datetime.ts +41 -0
  49. package/src/tools/builtin/file.ts +107 -0
  50. package/src/tools/builtin/index.ts +28 -0
  51. package/src/tools/builtin/shell.ts +43 -0
  52. package/src/tools/builtin/web.ts +35 -0
  53. package/src/tools/mcp-client.ts +131 -0
  54. package/tests/auto-learn.test.ts +105 -0
  55. package/tests/builtin-tools.test.ts +83 -0
  56. package/tests/cli.test.ts +46 -0
  57. package/tests/subagent.test.ts +130 -0
  58. package/tests/telegram-discord.test.ts +60 -0
package/src/core/agent.ts CHANGED
@@ -1,152 +1,295 @@
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
-
6
- export class BaseAgent extends EventEmitter implements IAgent {
7
- readonly name: string;
8
- private _state: AgentState = 'init';
9
- private skills: Map<string, ISkill> = new Map();
10
- private channels: IChannel[] = [];
11
- private memory: MemoryStore;
12
- private _provider: LLMProvider;
13
- private systemPrompt: string;
14
- private historyLimit: number;
15
-
16
- constructor(options: {
17
- name: string;
18
- systemPrompt?: string;
19
- provider?: string;
20
- model?: string;
21
- memory?: MemoryStore;
22
- historyLimit?: number;
23
- }) {
24
- super();
25
- this.name = options.name;
26
- this.systemPrompt = options.systemPrompt ?? 'You are a helpful AI agent.';
27
- this.memory = options.memory ?? new InMemoryStore();
28
- this._provider = createProvider(options.provider ?? 'openai', options.model);
29
- this.historyLimit = options.historyLimit ?? 50;
30
- }
31
-
32
- get state(): AgentState {
33
- return this._state;
34
- }
35
-
36
- get provider(): LLMProvider {
37
- return this._provider;
38
- }
39
-
40
- getSystemPrompt(): string {
41
- return this.systemPrompt;
42
- }
43
-
44
- getMemory(): MemoryStore {
45
- return this.memory;
46
- }
47
-
48
- private transition(to: AgentState): void {
49
- const from = this._state;
50
- this._state = to;
51
- this.emit('state:change', from, to);
52
- }
53
-
54
- async init(): Promise<void> {
55
- this.transition('ready');
56
- }
57
-
58
- async start(): Promise<void> {
59
- if (this._state !== 'ready') {
60
- throw new Error(`Cannot start agent in state: ${this._state}`);
61
- }
62
- for (const channel of this.channels) {
63
- channel.onMessage((msg) => this.handleMessage(msg));
64
- await channel.start();
65
- }
66
- this.transition('running');
67
- }
68
-
69
- async stop(): Promise<void> {
70
- for (const channel of this.channels) {
71
- await channel.stop();
72
- }
73
- this.transition('stopped');
74
- }
75
-
76
- registerSkill(skill: ISkill): void {
77
- this.skills.set(skill.name, skill);
78
- }
79
-
80
- bindChannel(channel: IChannel): void {
81
- this.channels.push(channel);
82
- }
83
-
84
- getChannels(): IChannel[] {
85
- return this.channels;
86
- }
87
-
88
- async handleMessage(message: Message): Promise<Message> {
89
- this.emit('message:in', message);
90
-
91
- const sessionId = (message.metadata?.sessionId as string) ?? 'default';
92
- await this.memory.addMessage(sessionId, message);
93
-
94
- const context: AgentContext = {
95
- agentName: this.name,
96
- sessionId,
97
- messages: (await this.memory.getConversation(sessionId)).slice(-this.historyLimit),
98
- memory: this.memory,
99
- metadata: {},
100
- };
101
-
102
- // Try skills first
103
- for (const [name, skill] of this.skills) {
104
- try {
105
- const result = await skill.execute(context, message);
106
- this.emit('skill:execute', name, result);
107
- if (result.handled && result.response) {
108
- const response = this.createResponse(result.response, message);
109
- await this.memory.addMessage(sessionId, response);
110
- this.emit('message:out', response);
111
- return response;
112
- }
113
- } catch (err) {
114
- this.emit('error', err instanceof Error ? err : new Error(String(err)));
115
- }
116
- }
117
-
118
- // Fall back to LLM
119
- const llmResponse = await this._provider.chat(context.messages, this.systemPrompt);
120
- const response = this.createResponse(llmResponse, message);
121
- await this.memory.addMessage(sessionId, response);
122
- this.emit('message:out', response);
123
- return response;
124
- }
125
-
126
- async *handleMessageStream(message: Message): AsyncIterable<string> {
127
- const sessionId = (message.metadata?.sessionId as string) ?? 'default';
128
- await this.memory.addMessage(sessionId, message);
129
-
130
- const history = (await this.memory.getConversation(sessionId)).slice(-this.historyLimit);
131
-
132
- let fullResponse = '';
133
- for await (const chunk of this._provider.chatStream(history, this.systemPrompt)) {
134
- fullResponse += chunk;
135
- yield chunk;
136
- }
137
-
138
- const response = this.createResponse(fullResponse, message);
139
- await this.memory.addMessage(sessionId, response);
140
- this.emit('message:out', response);
141
- }
142
-
143
- private createResponse(content: string, inReplyTo: Message): Message {
144
- return {
145
- id: `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
146
- role: 'assistant',
147
- content,
148
- timestamp: Date.now(),
149
- metadata: { inReplyTo: inReplyTo.id },
150
- };
151
- }
152
- }
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
+
10
+ export class BaseAgent extends EventEmitter implements IAgent {
11
+ readonly name: string;
12
+ private _state: AgentState = 'init';
13
+ private skills: Map<string, ISkill> = new Map();
14
+ private channels: IChannel[] = [];
15
+ private memory: MemoryStore;
16
+ private _provider: LLMProvider;
17
+ private systemPrompt: string;
18
+ private historyLimit: number;
19
+ private toolRegistry: MCPToolRegistry = new MCPToolRegistry();
20
+ private maxToolRounds: number;
21
+ private skillLearner?: SkillLearner;
22
+ private autoLearnConfig: { enabled: boolean; minConversationLength: number; improveOnUse: boolean };
23
+ private _subAgentManager?: SubAgentManager;
24
+
25
+ constructor(options: {
26
+ name: string;
27
+ systemPrompt?: string;
28
+ provider?: string;
29
+ model?: string;
30
+ memory?: MemoryStore;
31
+ historyLimit?: number;
32
+ skillsDir?: string;
33
+ learning?: {
34
+ autoSkillCreation?: boolean;
35
+ minConversationLength?: number;
36
+ improveOnUse?: boolean;
37
+ };
38
+ maxToolRounds?: number;
39
+ }) {
40
+ super();
41
+ this.name = options.name;
42
+ this.systemPrompt = options.systemPrompt ?? 'You are a helpful AI agent.';
43
+ this.memory = options.memory ?? new InMemoryStore();
44
+ this._provider = createProvider(options.provider ?? 'openai', options.model);
45
+ this.historyLimit = options.historyLimit ?? 50;
46
+ this.maxToolRounds = options.maxToolRounds ?? 10;
47
+ this.autoLearnConfig = {
48
+ enabled: options.learning?.autoSkillCreation !== false,
49
+ minConversationLength: options.learning?.minConversationLength ?? 3,
50
+ improveOnUse: options.learning?.improveOnUse !== false,
51
+ };
52
+ if (options.skillsDir) {
53
+ this.skillLearner = new SkillLearner(options.skillsDir);
54
+ }
55
+ }
56
+
57
+ get state(): AgentState {
58
+ return this._state;
59
+ }
60
+
61
+ get provider(): LLMProvider {
62
+ return this._provider;
63
+ }
64
+
65
+ getSystemPrompt(): string {
66
+ return this.systemPrompt;
67
+ }
68
+
69
+ getMemory(): MemoryStore {
70
+ return this.memory;
71
+ }
72
+
73
+ getSkillLearner(): SkillLearner | undefined {
74
+ return this.skillLearner;
75
+ }
76
+
77
+ getToolRegistry(): MCPToolRegistry {
78
+ return this.toolRegistry;
79
+ }
80
+
81
+ registerTool(tool: MCPTool): void {
82
+ this.toolRegistry.register(tool);
83
+ }
84
+
85
+ private transition(to: AgentState): void {
86
+ const from = this._state;
87
+ this._state = to;
88
+ this.emit('state:change', from, to);
89
+ }
90
+
91
+ async init(): Promise<void> {
92
+ if (this.skillLearner) {
93
+ await this.skillLearner.loadLearnedSkills();
94
+ }
95
+ this.transition('ready');
96
+ }
97
+
98
+ async start(): Promise<void> {
99
+ if (this._state !== 'ready') {
100
+ throw new Error(`Cannot start agent in state: ${this._state}`);
101
+ }
102
+ for (const channel of this.channels) {
103
+ channel.onMessage((msg) => this.handleMessage(msg));
104
+ await channel.start();
105
+ }
106
+ this.transition('running');
107
+ }
108
+
109
+ async stop(): Promise<void> {
110
+ for (const channel of this.channels) {
111
+ await channel.stop();
112
+ }
113
+ this.transition('stopped');
114
+ }
115
+
116
+ registerSkill(skill: ISkill): void {
117
+ this.skills.set(skill.name, skill);
118
+ }
119
+
120
+ bindChannel(channel: IChannel): void {
121
+ this.channels.push(channel);
122
+ }
123
+
124
+ getChannels(): IChannel[] {
125
+ return this.channels;
126
+ }
127
+
128
+ private getSubAgentManager(): SubAgentManager {
129
+ if (!this._subAgentManager) {
130
+ this._subAgentManager = new SubAgentManager();
131
+ }
132
+ return this._subAgentManager;
133
+ }
134
+
135
+ async spawnSubAgent(config: SubAgentConfig): Promise<SubAgentResult> {
136
+ return this.getSubAgentManager().spawn(config, this._provider);
137
+ }
138
+
139
+ async spawnParallel(configs: SubAgentConfig[]): Promise<SubAgentResult[]> {
140
+ return this.getSubAgentManager().spawnParallel(configs, this._provider);
141
+ }
142
+
143
+ async handleMessage(message: Message): Promise<Message> {
144
+ this.emit('message:in', message);
145
+
146
+ const sessionId = (message.metadata?.sessionId as string) ?? 'default';
147
+ await this.memory.addMessage(sessionId, message);
148
+
149
+ const context: AgentContext = {
150
+ agentName: this.name,
151
+ sessionId,
152
+ messages: (await this.memory.getConversation(sessionId)).slice(-this.historyLimit),
153
+ memory: this.memory,
154
+ metadata: {},
155
+ };
156
+
157
+ // Try skills first
158
+ for (const [name, skill] of this.skills) {
159
+ try {
160
+ const result = await skill.execute(context, message);
161
+ this.emit('skill:execute', name, result);
162
+ if (result.handled && result.response) {
163
+ const response = this.createResponse(result.response, message);
164
+ await this.memory.addMessage(sessionId, response);
165
+ this.emit('message:out', response);
166
+ return response;
167
+ }
168
+ } catch (err) {
169
+ this.emit('error', err instanceof Error ? err : new Error(String(err)));
170
+ }
171
+ }
172
+
173
+ // Check if a learned skill matches — prepend instructions to system prompt
174
+ let effectiveSystemPrompt = this.systemPrompt;
175
+ const matchedSkill = this.skillLearner?.matchSkill(message.content);
176
+ if (matchedSkill) {
177
+ matchedSkill.usageCount++;
178
+ matchedSkill.lastUsed = new Date();
179
+ effectiveSystemPrompt = `[Learned Skill: ${matchedSkill.name}]\n${matchedSkill.instructions}\n\n${this.systemPrompt}`;
180
+ this.emit('skill:matched', matchedSkill);
181
+ }
182
+
183
+ // Fall back to LLM with tool use loop
184
+ const tools = this.toolRegistry.list();
185
+ const llmMessages = [...context.messages];
186
+ let finalResponse = '';
187
+
188
+ for (let round = 0; round <= this.maxToolRounds; round++) {
189
+ const llmResponse = await this._provider.chat(
190
+ llmMessages,
191
+ effectiveSystemPrompt,
192
+ { tools: tools.length > 0 ? tools : undefined },
193
+ );
194
+
195
+ const toolCall = this.parseToolCall(llmResponse);
196
+ if (!toolCall || tools.length === 0 || round === this.maxToolRounds) {
197
+ finalResponse = llmResponse;
198
+ break;
199
+ }
200
+
201
+ // Execute tool
202
+ const toolResult = await this.toolRegistry.execute(toolCall.name, toolCall.arguments, context);
203
+ this.emit('tool:execute', toolCall.name, toolResult);
204
+
205
+ // Add tool call and result to messages for next round
206
+ llmMessages.push({
207
+ id: `tool_call_${Date.now()}`,
208
+ role: 'assistant',
209
+ content: llmResponse,
210
+ timestamp: Date.now(),
211
+ });
212
+ llmMessages.push({
213
+ id: `tool_result_${Date.now()}`,
214
+ role: 'user',
215
+ content: `[Tool Result for ${toolCall.name}]: ${toolResult.content}`,
216
+ timestamp: Date.now(),
217
+ });
218
+ }
219
+
220
+ const response = this.createResponse(finalResponse, message);
221
+ await this.memory.addMessage(sessionId, response);
222
+ this.emit('message:out', response);
223
+
224
+ // After response, check if we should learn a skill
225
+ if (
226
+ this.skillLearner &&
227
+ this.autoLearnConfig.enabled &&
228
+ context.messages.length >= this.autoLearnConfig.minConversationLength
229
+ ) {
230
+ this.skillLearner
231
+ .analyzeForSkillCreation(context.messages, this._provider)
232
+ .then(async (learnedSkill) => {
233
+ if (learnedSkill) {
234
+ await this.skillLearner!.saveSkill(learnedSkill);
235
+ this.emit('skill:learned', learnedSkill);
236
+ }
237
+ })
238
+ .catch(() => {});
239
+ }
240
+
241
+ // Improve matched skill after use
242
+ if (matchedSkill && this.skillLearner && this.autoLearnConfig.improveOnUse) {
243
+ this.skillLearner
244
+ .improveSkill(matchedSkill, context.messages, this._provider)
245
+ .then(() => this.skillLearner!.saveSkill(matchedSkill))
246
+ .catch(() => {});
247
+ }
248
+
249
+ return response;
250
+ }
251
+
252
+ private parseToolCall(response: string): { name: string; arguments: Record<string, unknown> } | null {
253
+ try {
254
+ const parsed = JSON.parse(response);
255
+ if (parsed.tool_call) return parsed.tool_call;
256
+ if (parsed.name && parsed.arguments !== undefined) return parsed;
257
+ } catch { /* not JSON */ }
258
+
259
+ const match = response.match(/<tool_call>\s*(\{[\s\S]*?\})\s*<\/tool_call>/);
260
+ if (match) {
261
+ try {
262
+ const parsed = JSON.parse(match[1]);
263
+ if (parsed.name) return parsed;
264
+ } catch { /* not valid JSON */ }
265
+ }
266
+ return null;
267
+ }
268
+
269
+ async *handleMessageStream(message: Message): AsyncIterable<string> {
270
+ const sessionId = (message.metadata?.sessionId as string) ?? 'default';
271
+ await this.memory.addMessage(sessionId, message);
272
+
273
+ const history = (await this.memory.getConversation(sessionId)).slice(-this.historyLimit);
274
+
275
+ let fullResponse = '';
276
+ for await (const chunk of this._provider.chatStream(history, this.systemPrompt)) {
277
+ fullResponse += chunk;
278
+ yield chunk;
279
+ }
280
+
281
+ const response = this.createResponse(fullResponse, message);
282
+ await this.memory.addMessage(sessionId, response);
283
+ this.emit('message:out', response);
284
+ }
285
+
286
+ private createResponse(content: string, inReplyTo: Message): Message {
287
+ return {
288
+ id: `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
289
+ role: 'assistant',
290
+ content,
291
+ timestamp: Date.now(),
292
+ metadata: { inReplyTo: inReplyTo.id },
293
+ };
294
+ }
295
+ }
@@ -7,6 +7,8 @@ import { WebSocketChannel } from '../channels/websocket';
7
7
  import { DeepBrainMemoryStore } from '../memory/deepbrain';
8
8
  import { Analytics } from '../analytics';
9
9
  import type { OADDocument } from '../schema/oad';
10
+ import { Scheduler } from './scheduler';
11
+ import type { CronJob } from './scheduler';
10
12
  import type { ISkill, MemoryStore, Message } from './types';
11
13
  import type { Response } from 'express';
12
14
 
@@ -27,6 +29,7 @@ export class AgentRuntime {
27
29
  private shutdownHandlers: (() => Promise<void>)[] = [];
28
30
  private isShuttingDown = false;
29
31
  private analytics: Analytics = new Analytics();
32
+ private scheduler: Scheduler | null = null;
30
33
 
31
34
  async loadConfig(filePath: string): Promise<OADDocument> {
32
35
  this.config = loadOAD(filePath);
@@ -122,6 +125,42 @@ export class AgentRuntime {
122
125
  });
123
126
 
124
127
  this.logger.info('Agent initialized', { name: cfg.metadata.name });
128
+
129
+ // Initialize scheduler if jobs are configured
130
+ const schedulerCfg = (cfg.spec as any).scheduler;
131
+ if (schedulerCfg?.jobs && Array.isArray(schedulerCfg.jobs) && schedulerCfg.jobs.length > 0) {
132
+ this.scheduler = new Scheduler(async (job: CronJob) => {
133
+ this.logger.info('Scheduler firing job', { name: job.name, task: job.task });
134
+ if (this.agent) {
135
+ const msg: Message = {
136
+ id: `cron-${job.id}-${Date.now()}`,
137
+ role: 'user',
138
+ content: job.task,
139
+ timestamp: Date.now(),
140
+ metadata: { source: 'scheduler', jobId: job.id, jobName: job.name },
141
+ };
142
+ try {
143
+ await this.agent.handleMessage(msg);
144
+ } catch (err) {
145
+ this.logger.error('Scheduler job failed', { name: job.name, error: err instanceof Error ? err.message : String(err) });
146
+ }
147
+ }
148
+ });
149
+
150
+ for (let i = 0; i < schedulerCfg.jobs.length; i++) {
151
+ const j = schedulerCfg.jobs[i];
152
+ const id = j.id || j.name?.toLowerCase().replace(/\s+/g, '-') || `job-${i}`;
153
+ this.scheduler.addJob({
154
+ id,
155
+ name: j.name || id,
156
+ schedule: j.schedule,
157
+ task: j.task || '',
158
+ enabled: j.enabled !== false,
159
+ });
160
+ }
161
+ this.logger.info('Scheduler configured', { jobs: schedulerCfg.jobs.length });
162
+ }
163
+
125
164
  return this.agent;
126
165
  }
127
166
 
@@ -129,12 +168,20 @@ export class AgentRuntime {
129
168
  if (!this.agent) throw new Error('Agent not initialized.');
130
169
  this.setupGracefulShutdown();
131
170
  await this.agent.start();
171
+ if (this.scheduler) {
172
+ this.scheduler.start();
173
+ this.logger.info('Scheduler started');
174
+ }
132
175
  this.logger.info('Agent started');
133
176
  }
134
177
 
135
178
  async stop(): Promise<void> {
136
179
  if (!this.agent) return;
137
180
  this.logger.info('Stopping agent...');
181
+ if (this.scheduler) {
182
+ this.scheduler.stop();
183
+ this.logger.info('Scheduler stopped');
184
+ }
138
185
  await this.agent.stop();
139
186
  for (const handler of this.shutdownHandlers) {
140
187
  await handler();