opc-agent 0.9.0 → 1.1.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.
@@ -1,10 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PluginManager = void 0;
4
+ exports.createLoggingPlugin = createLoggingPlugin;
5
+ exports.createAnalyticsPlugin = createAnalyticsPlugin;
6
+ exports.createRateLimitPlugin = createRateLimitPlugin;
7
+ const logger_1 = require("../core/logger");
4
8
  class PluginManager {
5
9
  plugins = new Map();
10
+ logger = new logger_1.Logger('plugins');
6
11
  register(plugin) {
12
+ if (this.plugins.has(plugin.name)) {
13
+ this.logger.warn(`Plugin "${plugin.name}" already registered, replacing`);
14
+ }
7
15
  this.plugins.set(plugin.name, plugin);
16
+ this.logger.info(`Plugin registered: ${plugin.name}@${plugin.version}`);
8
17
  }
9
18
  unregister(name) {
10
19
  this.plugins.delete(name);
@@ -14,9 +23,7 @@ class PluginManager {
14
23
  }
15
24
  list() {
16
25
  return Array.from(this.plugins.values()).map(({ name, version, description }) => ({
17
- name,
18
- version,
19
- description,
26
+ name, version, description,
20
27
  }));
21
28
  }
22
29
  has(name) {
@@ -26,10 +33,58 @@ class PluginManager {
26
33
  for (const plugin of this.plugins.values()) {
27
34
  const hook = plugin.hooks?.[hookName];
28
35
  if (hook) {
29
- await hook(...args);
36
+ try {
37
+ await hook(...args);
38
+ }
39
+ catch (err) {
40
+ this.logger.error(`Plugin "${plugin.name}" hook "${hookName}" failed`, {
41
+ error: err instanceof Error ? err.message : String(err),
42
+ });
43
+ // Don't let one plugin break others
44
+ }
30
45
  }
31
46
  }
32
47
  }
48
+ async runOnInit() {
49
+ await this.runHook('onInit');
50
+ await this.runHook('beforeInit');
51
+ await this.runHook('afterInit');
52
+ }
53
+ async runOnMessage(message) {
54
+ let msg = message;
55
+ for (const plugin of this.plugins.values()) {
56
+ if (plugin.hooks?.onMessage) {
57
+ const result = await plugin.hooks.onMessage(msg);
58
+ if (result)
59
+ msg = result;
60
+ }
61
+ if (plugin.hooks?.beforeMessage) {
62
+ await plugin.hooks.beforeMessage({ content: msg.content });
63
+ }
64
+ }
65
+ return msg;
66
+ }
67
+ async runOnResponse(message, response) {
68
+ let resp = response;
69
+ for (const plugin of this.plugins.values()) {
70
+ if (plugin.hooks?.onResponse) {
71
+ const result = await plugin.hooks.onResponse(message, resp);
72
+ if (result)
73
+ resp = result;
74
+ }
75
+ if (plugin.hooks?.afterMessage) {
76
+ await plugin.hooks.afterMessage({ content: message.content }, { content: resp.content });
77
+ }
78
+ }
79
+ return resp;
80
+ }
81
+ async runOnError(error, context) {
82
+ await this.runHook('onError', error, context);
83
+ }
84
+ async runOnShutdown() {
85
+ await this.runHook('onShutdown');
86
+ await this.runHook('beforeShutdown');
87
+ }
33
88
  getAllSkills() {
34
89
  const skills = [];
35
90
  for (const plugin of this.plugins.values()) {
@@ -56,4 +111,54 @@ class PluginManager {
56
111
  }
57
112
  }
58
113
  exports.PluginManager = PluginManager;
114
+ // ── Built-in Plugins ────────────────────────────────────────
115
+ function createLoggingPlugin() {
116
+ const logger = new logger_1.Logger('agent:messages');
117
+ return {
118
+ name: 'logging',
119
+ version: '1.0.0',
120
+ description: 'Logs all messages and responses',
121
+ hooks: {
122
+ onInit: async () => { logger.info('Agent initialized'); },
123
+ onMessage: async (msg) => { logger.info(`← ${msg.role}: ${msg.content.slice(0, 100)}`); return undefined; },
124
+ onResponse: async (_msg, resp) => { logger.info(`→ ${resp.role}: ${resp.content.slice(0, 100)}`); return undefined; },
125
+ onError: async (err) => { logger.error(`Error: ${err.message}`); },
126
+ onShutdown: async () => { logger.info('Agent shutting down'); },
127
+ },
128
+ };
129
+ }
130
+ function createAnalyticsPlugin() {
131
+ const stats = { messages: 0, errors: 0, startedAt: 0 };
132
+ return {
133
+ name: 'analytics',
134
+ version: '1.0.0',
135
+ description: 'Tracks message counts and error rates',
136
+ hooks: {
137
+ onInit: async () => { stats.startedAt = Date.now(); },
138
+ onMessage: async () => { stats.messages++; return undefined; },
139
+ onError: async () => { stats.errors++; },
140
+ },
141
+ };
142
+ }
143
+ function createRateLimitPlugin(maxPerMinute = 60) {
144
+ const timestamps = [];
145
+ return {
146
+ name: 'rate-limit',
147
+ version: '1.0.0',
148
+ description: `Rate limits to ${maxPerMinute} messages/minute`,
149
+ hooks: {
150
+ onMessage: async () => {
151
+ const now = Date.now();
152
+ const windowStart = now - 60_000;
153
+ while (timestamps.length > 0 && timestamps[0] < windowStart)
154
+ timestamps.shift();
155
+ if (timestamps.length >= maxPerMinute) {
156
+ throw new Error('Rate limit exceeded. Please slow down.');
157
+ }
158
+ timestamps.push(now);
159
+ return undefined;
160
+ },
161
+ },
162
+ };
163
+ }
59
164
  //# sourceMappingURL=index.js.map
@@ -77,6 +77,16 @@ export declare const HITLSchema: z.ZodObject<{
77
77
  defaultTimeoutMs?: number | undefined;
78
78
  defaultAction?: "approve" | "deny" | undefined;
79
79
  }>;
80
+ export declare const PluginRefSchema: z.ZodObject<{
81
+ name: z.ZodString;
82
+ config: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
83
+ }, "strip", z.ZodTypeAny, {
84
+ name: string;
85
+ config?: Record<string, unknown> | undefined;
86
+ }, {
87
+ name: string;
88
+ config?: Record<string, unknown> | undefined;
89
+ }>;
80
90
  export declare const AuthSchema: z.ZodObject<{
81
91
  enabled: z.ZodDefault<z.ZodBoolean>;
82
92
  apiKeys: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
@@ -479,6 +489,16 @@ export declare const SpecSchema: z.ZodObject<{
479
489
  apiKeys?: string[] | undefined;
480
490
  sessionIsolation?: boolean | undefined;
481
491
  }>>;
492
+ plugins: z.ZodOptional<z.ZodArray<z.ZodObject<{
493
+ name: z.ZodString;
494
+ config: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
495
+ }, "strip", z.ZodTypeAny, {
496
+ name: string;
497
+ config?: Record<string, unknown> | undefined;
498
+ }, {
499
+ name: string;
500
+ config?: Record<string, unknown> | undefined;
501
+ }>, "many">>;
482
502
  }, "strip", z.ZodTypeAny, {
483
503
  model: string;
484
504
  skills: {
@@ -552,6 +572,10 @@ export declare const SpecSchema: z.ZodObject<{
552
572
  defaultTimeoutMs: number;
553
573
  defaultAction: "approve" | "deny";
554
574
  } | undefined;
575
+ plugins?: {
576
+ name: string;
577
+ config?: Record<string, unknown> | undefined;
578
+ }[] | undefined;
555
579
  }, {
556
580
  auth?: {
557
581
  enabled?: boolean | undefined;
@@ -625,6 +649,10 @@ export declare const SpecSchema: z.ZodObject<{
625
649
  defaultTimeoutMs?: number | undefined;
626
650
  defaultAction?: "approve" | "deny" | undefined;
627
651
  } | undefined;
652
+ plugins?: {
653
+ name: string;
654
+ config?: Record<string, unknown> | undefined;
655
+ }[] | undefined;
628
656
  }>;
629
657
  export declare const OADSchema: z.ZodObject<{
630
658
  apiVersion: z.ZodLiteral<"opc/v1">;
@@ -879,6 +907,16 @@ export declare const OADSchema: z.ZodObject<{
879
907
  apiKeys?: string[] | undefined;
880
908
  sessionIsolation?: boolean | undefined;
881
909
  }>>;
910
+ plugins: z.ZodOptional<z.ZodArray<z.ZodObject<{
911
+ name: z.ZodString;
912
+ config: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
913
+ }, "strip", z.ZodTypeAny, {
914
+ name: string;
915
+ config?: Record<string, unknown> | undefined;
916
+ }, {
917
+ name: string;
918
+ config?: Record<string, unknown> | undefined;
919
+ }>, "many">>;
882
920
  }, "strip", z.ZodTypeAny, {
883
921
  model: string;
884
922
  skills: {
@@ -952,6 +990,10 @@ export declare const OADSchema: z.ZodObject<{
952
990
  defaultTimeoutMs: number;
953
991
  defaultAction: "approve" | "deny";
954
992
  } | undefined;
993
+ plugins?: {
994
+ name: string;
995
+ config?: Record<string, unknown> | undefined;
996
+ }[] | undefined;
955
997
  }, {
956
998
  auth?: {
957
999
  enabled?: boolean | undefined;
@@ -1025,6 +1067,10 @@ export declare const OADSchema: z.ZodObject<{
1025
1067
  defaultTimeoutMs?: number | undefined;
1026
1068
  defaultAction?: "approve" | "deny" | undefined;
1027
1069
  } | undefined;
1070
+ plugins?: {
1071
+ name: string;
1072
+ config?: Record<string, unknown> | undefined;
1073
+ }[] | undefined;
1028
1074
  }>;
1029
1075
  }, "strip", z.ZodTypeAny, {
1030
1076
  apiVersion: "opc/v1";
@@ -1115,6 +1161,10 @@ export declare const OADSchema: z.ZodObject<{
1115
1161
  defaultTimeoutMs: number;
1116
1162
  defaultAction: "approve" | "deny";
1117
1163
  } | undefined;
1164
+ plugins?: {
1165
+ name: string;
1166
+ config?: Record<string, unknown> | undefined;
1167
+ }[] | undefined;
1118
1168
  };
1119
1169
  }, {
1120
1170
  apiVersion: "opc/v1";
@@ -1205,6 +1255,10 @@ export declare const OADSchema: z.ZodObject<{
1205
1255
  defaultTimeoutMs?: number | undefined;
1206
1256
  defaultAction?: "approve" | "deny" | undefined;
1207
1257
  } | undefined;
1258
+ plugins?: {
1259
+ name: string;
1260
+ config?: Record<string, unknown> | undefined;
1261
+ }[] | undefined;
1208
1262
  };
1209
1263
  }>;
1210
1264
  export type OADDocument = z.infer<typeof OADSchema>;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.OADSchema = exports.SpecSchema = exports.StreamingSchema = exports.RoomSchema = exports.MetadataSchema = exports.MarketplaceSchema = exports.ProviderSchema = exports.DTVSchema = exports.TrustLevel = exports.MemorySchema = exports.LongTermMemorySchema = exports.ChannelSchema = exports.AuthSchema = exports.HITLSchema = exports.WebhookSchema = exports.VoiceSchema = exports.WorkflowSchema = exports.WorkflowStepSchema = exports.SkillRefSchema = void 0;
3
+ exports.OADSchema = exports.SpecSchema = exports.StreamingSchema = exports.RoomSchema = exports.MetadataSchema = exports.MarketplaceSchema = exports.ProviderSchema = exports.DTVSchema = exports.TrustLevel = exports.MemorySchema = exports.LongTermMemorySchema = exports.ChannelSchema = exports.AuthSchema = exports.PluginRefSchema = exports.HITLSchema = exports.WebhookSchema = exports.VoiceSchema = exports.WorkflowSchema = exports.WorkflowStepSchema = exports.SkillRefSchema = void 0;
4
4
  const zod_1 = require("zod");
5
5
  // ─── OAD Schema v1 ───────────────────────────────────────────
6
6
  exports.SkillRefSchema = zod_1.z.object({
@@ -43,6 +43,10 @@ exports.HITLSchema = zod_1.z.object({
43
43
  defaultTimeoutMs: zod_1.z.number().default(60000),
44
44
  defaultAction: zod_1.z.enum(['approve', 'deny']).default('deny'),
45
45
  });
46
+ exports.PluginRefSchema = zod_1.z.object({
47
+ name: zod_1.z.string(),
48
+ config: zod_1.z.record(zod_1.z.unknown()).optional(),
49
+ });
46
50
  exports.AuthSchema = zod_1.z.object({
47
51
  enabled: zod_1.z.boolean().default(false),
48
52
  apiKeys: zod_1.z.array(zod_1.z.string()).default([]),
@@ -115,6 +119,7 @@ exports.SpecSchema = zod_1.z.object({
115
119
  webhook: exports.WebhookSchema.optional(),
116
120
  hitl: exports.HITLSchema.optional(),
117
121
  auth: exports.AuthSchema.optional(),
122
+ plugins: zod_1.z.array(exports.PluginRefSchema).optional(),
118
123
  });
119
124
  exports.OADSchema = zod_1.z.object({
120
125
  apiVersion: zod_1.z.literal('opc/v1'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opc-agent",
3
- "version": "0.9.0",
3
+ "version": "1.1.0",
4
4
  "description": "Open Agent Framework — Build, test, and run AI Agents for business workstations",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,192 @@
1
+ import { BaseChannel } from './index';
2
+ import type { Message } from '../core/types';
3
+
4
+ /**
5
+ * Discord Channel — v1.1.0
6
+ *
7
+ * Supports:
8
+ * - Discord Bot via Gateway (WebSocket) or HTTP interactions
9
+ * - Slash commands, message content intent
10
+ * - Thread-based conversations
11
+ * - Reactions, embeds
12
+ *
13
+ * Env vars:
14
+ * DISCORD_BOT_TOKEN — bot token
15
+ * DISCORD_APPLICATION_ID — application ID for slash commands
16
+ */
17
+
18
+ export interface DiscordChannelConfig {
19
+ /** Bot token */
20
+ botToken?: string;
21
+ /** Application ID */
22
+ applicationId?: string;
23
+ /** Guild IDs to register slash commands (empty = global) */
24
+ guildIds?: string[];
25
+ /** Whether to use threads for conversations */
26
+ useThreads?: boolean;
27
+ }
28
+
29
+ export class DiscordChannel extends BaseChannel {
30
+ readonly type = 'discord';
31
+ private config: DiscordChannelConfig;
32
+ private ws: import('ws').WebSocket | null = null;
33
+ private heartbeatInterval: ReturnType<typeof setInterval> | null = null;
34
+ private sequenceNumber: number | null = null;
35
+ private sessionId: string | null = null;
36
+ private resumeUrl: string | null = null;
37
+
38
+ constructor(config: DiscordChannelConfig = {}) {
39
+ super();
40
+ this.config = {
41
+ botToken: config.botToken ?? process.env.DISCORD_BOT_TOKEN ?? '',
42
+ applicationId: config.applicationId ?? process.env.DISCORD_APPLICATION_ID ?? '',
43
+ guildIds: config.guildIds ?? [],
44
+ useThreads: config.useThreads ?? true,
45
+ };
46
+ }
47
+
48
+ async start(): Promise<void> {
49
+ if (!this.config.botToken) {
50
+ console.warn('[DiscordChannel] No bot token. Set DISCORD_BOT_TOKEN.');
51
+ return;
52
+ }
53
+
54
+ // Get gateway URL
55
+ const gatewayResp = await fetch('https://discord.com/api/v10/gateway/bot', {
56
+ headers: { Authorization: `Bot ${this.config.botToken}` },
57
+ });
58
+ const gatewayData = await gatewayResp.json() as { url: string };
59
+ const wsUrl = `${gatewayData.url}?v=10&encoding=json`;
60
+
61
+ const { WebSocket } = await import('ws');
62
+ this.ws = new WebSocket(wsUrl);
63
+
64
+ this.ws.on('message', async (data: Buffer) => {
65
+ const payload = JSON.parse(data.toString());
66
+ this.sequenceNumber = payload.s ?? this.sequenceNumber;
67
+
68
+ switch (payload.op) {
69
+ case 10: // Hello
70
+ this.startHeartbeat(payload.d.heartbeat_interval);
71
+ this.identify();
72
+ break;
73
+ case 11: // Heartbeat ACK
74
+ break;
75
+ case 0: // Dispatch
76
+ if (payload.t === 'READY') {
77
+ this.sessionId = payload.d.session_id;
78
+ this.resumeUrl = payload.d.resume_gateway_url;
79
+ console.log(`[DiscordChannel] Connected as ${payload.d.user.username}`);
80
+ } else if (payload.t === 'MESSAGE_CREATE') {
81
+ await this.handleMessage(payload.d);
82
+ }
83
+ break;
84
+ }
85
+ });
86
+
87
+ this.ws.on('close', (code: number) => {
88
+ console.log(`[DiscordChannel] WebSocket closed: ${code}`);
89
+ this.stopHeartbeat();
90
+ // Auto-reconnect after 5s for resumable codes
91
+ if (code !== 4004 && code !== 4014) {
92
+ setTimeout(() => this.start(), 5000);
93
+ }
94
+ });
95
+
96
+ this.ws.on('error', (err: Error) => {
97
+ console.error('[DiscordChannel] WebSocket error:', err.message);
98
+ });
99
+ }
100
+
101
+ async stop(): Promise<void> {
102
+ this.stopHeartbeat();
103
+ if (this.ws) {
104
+ this.ws.close(1000);
105
+ this.ws = null;
106
+ }
107
+ }
108
+
109
+ private identify(): void {
110
+ this.ws?.send(JSON.stringify({
111
+ op: 2,
112
+ d: {
113
+ token: this.config.botToken,
114
+ intents: (1 << 9) | (1 << 15), // GUILD_MESSAGES | MESSAGE_CONTENT
115
+ properties: {
116
+ os: process.platform,
117
+ browser: 'opc-agent',
118
+ device: 'opc-agent',
119
+ },
120
+ },
121
+ }));
122
+ }
123
+
124
+ private startHeartbeat(intervalMs: number): void {
125
+ this.stopHeartbeat();
126
+ // Send first heartbeat with jitter
127
+ setTimeout(() => {
128
+ this.sendHeartbeat();
129
+ this.heartbeatInterval = setInterval(() => this.sendHeartbeat(), intervalMs);
130
+ }, intervalMs * Math.random());
131
+ }
132
+
133
+ private stopHeartbeat(): void {
134
+ if (this.heartbeatInterval) {
135
+ clearInterval(this.heartbeatInterval);
136
+ this.heartbeatInterval = null;
137
+ }
138
+ }
139
+
140
+ private sendHeartbeat(): void {
141
+ this.ws?.send(JSON.stringify({ op: 1, d: this.sequenceNumber }));
142
+ }
143
+
144
+ private async handleMessage(d: Record<string, unknown>): Promise<void> {
145
+ // Ignore bot messages
146
+ const author = d.author as Record<string, unknown>;
147
+ if (author?.bot) return;
148
+ if (!d.content || !this.handler) return;
149
+
150
+ const msg: Message = {
151
+ id: `discord_${d.id}`,
152
+ role: 'user',
153
+ content: d.content as string,
154
+ timestamp: new Date(d.timestamp as string).getTime(),
155
+ metadata: {
156
+ sessionId: `discord_${d.channel_id}`,
157
+ chatId: d.channel_id as string,
158
+ userId: author.id as string,
159
+ platform: 'discord',
160
+ guildId: d.guild_id as string | undefined,
161
+ threadId: (d as Record<string, unknown>).thread?.toString(),
162
+ },
163
+ };
164
+
165
+ const response = await this.handler(msg);
166
+ await this.sendMessage(d.channel_id as string, response.content);
167
+ }
168
+
169
+ async sendMessage(channelId: string, content: string): Promise<void> {
170
+ // Discord max message length is 2000
171
+ const chunks = this.splitMessage(content, 2000);
172
+ for (const chunk of chunks) {
173
+ await fetch(`https://discord.com/api/v10/channels/${channelId}/messages`, {
174
+ method: 'POST',
175
+ headers: {
176
+ 'Content-Type': 'application/json',
177
+ Authorization: `Bot ${this.config.botToken}`,
178
+ },
179
+ body: JSON.stringify({ content: chunk }),
180
+ });
181
+ }
182
+ }
183
+
184
+ private splitMessage(text: string, maxLen: number): string[] {
185
+ if (text.length <= maxLen) return [text];
186
+ const parts: string[] = [];
187
+ for (let i = 0; i < text.length; i += maxLen) {
188
+ parts.push(text.slice(i, i + maxLen));
189
+ }
190
+ return parts;
191
+ }
192
+ }