opc-agent 3.0.0 → 4.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.
- package/README.md +30 -24
- package/dist/channels/dingtalk.d.ts +17 -0
- package/dist/channels/dingtalk.js +38 -0
- package/dist/channels/googlechat.d.ts +14 -0
- package/dist/channels/googlechat.js +37 -0
- package/dist/channels/imessage.d.ts +13 -0
- package/dist/channels/imessage.js +28 -0
- package/dist/channels/irc.d.ts +20 -0
- package/dist/channels/irc.js +71 -0
- package/dist/channels/line.d.ts +14 -0
- package/dist/channels/line.js +28 -0
- package/dist/channels/matrix.d.ts +15 -0
- package/dist/channels/matrix.js +28 -0
- package/dist/channels/mattermost.d.ts +18 -0
- package/dist/channels/mattermost.js +49 -0
- package/dist/channels/msteams.d.ts +14 -0
- package/dist/channels/msteams.js +28 -0
- package/dist/channels/nostr.d.ts +14 -0
- package/dist/channels/nostr.js +28 -0
- package/dist/channels/qq.d.ts +15 -0
- package/dist/channels/qq.js +28 -0
- package/dist/channels/signal.d.ts +14 -0
- package/dist/channels/signal.js +28 -0
- package/dist/channels/sms.d.ts +15 -0
- package/dist/channels/sms.js +28 -0
- package/dist/channels/twitch.d.ts +17 -0
- package/dist/channels/twitch.js +59 -0
- package/dist/channels/voice-call.d.ts +27 -0
- package/dist/channels/voice-call.js +82 -0
- package/dist/channels/whatsapp.d.ts +14 -0
- package/dist/channels/whatsapp.js +28 -0
- package/dist/cli.js +36 -0
- package/dist/core/api-server.d.ts +25 -0
- package/dist/core/api-server.js +286 -0
- package/dist/core/audio.d.ts +50 -0
- package/dist/core/audio.js +68 -0
- package/dist/core/context-discovery.d.ts +16 -0
- package/dist/core/context-discovery.js +107 -0
- package/dist/core/context-refs.d.ts +29 -0
- package/dist/core/context-refs.js +162 -0
- package/dist/core/gateway.d.ts +53 -0
- package/dist/core/gateway.js +80 -0
- package/dist/core/heartbeat.d.ts +19 -0
- package/dist/core/heartbeat.js +50 -0
- package/dist/core/hooks.d.ts +28 -0
- package/dist/core/hooks.js +82 -0
- package/dist/core/ide-bridge.d.ts +53 -0
- package/dist/core/ide-bridge.js +97 -0
- package/dist/core/node-network.d.ts +23 -0
- package/dist/core/node-network.js +77 -0
- package/dist/core/profiles.d.ts +27 -0
- package/dist/core/profiles.js +131 -0
- package/dist/core/sandbox.d.ts +25 -0
- package/dist/core/sandbox.js +84 -1
- package/dist/core/session-manager.d.ts +33 -0
- package/dist/core/session-manager.js +157 -0
- package/dist/core/vision.d.ts +45 -0
- package/dist/core/vision.js +177 -0
- package/dist/index.d.ts +64 -1
- package/dist/index.js +86 -3
- package/dist/memory/context-compressor.d.ts +43 -0
- package/dist/memory/context-compressor.js +167 -0
- package/dist/memory/index.d.ts +4 -0
- package/dist/memory/index.js +5 -1
- package/dist/memory/user-profiler.d.ts +50 -0
- package/dist/memory/user-profiler.js +201 -0
- package/dist/schema/oad.d.ts +12 -12
- package/dist/security/approvals.d.ts +53 -0
- package/dist/security/approvals.js +115 -0
- package/dist/security/elevated.d.ts +41 -0
- package/dist/security/elevated.js +89 -0
- package/dist/security/index.d.ts +6 -0
- package/dist/security/index.js +7 -1
- package/dist/security/secrets.d.ts +34 -0
- package/dist/security/secrets.js +115 -0
- package/dist/tools/builtin/browser.d.ts +47 -0
- package/dist/tools/builtin/browser.js +284 -0
- package/dist/tools/builtin/home-assistant.d.ts +12 -0
- package/dist/tools/builtin/home-assistant.js +126 -0
- package/dist/tools/builtin/index.d.ts +6 -1
- package/dist/tools/builtin/index.js +18 -2
- package/dist/tools/builtin/rl-tools.d.ts +13 -0
- package/dist/tools/builtin/rl-tools.js +228 -0
- package/dist/tools/builtin/vision.d.ts +6 -0
- package/dist/tools/builtin/vision.js +61 -0
- package/package.json +3 -3
- package/src/channels/dingtalk.ts +46 -0
- package/src/channels/googlechat.ts +42 -0
- package/src/channels/imessage.ts +32 -0
- package/src/channels/irc.ts +82 -0
- package/src/channels/line.ts +33 -0
- package/src/channels/matrix.ts +34 -0
- package/src/channels/mattermost.ts +57 -0
- package/src/channels/msteams.ts +33 -0
- package/src/channels/nostr.ts +33 -0
- package/src/channels/qq.ts +34 -0
- package/src/channels/signal.ts +33 -0
- package/src/channels/sms.ts +34 -0
- package/src/channels/twitch.ts +65 -0
- package/src/channels/voice-call.ts +100 -0
- package/src/channels/whatsapp.ts +33 -0
- package/src/cli.ts +40 -0
- package/src/core/api-server.ts +277 -0
- package/src/core/audio.ts +98 -0
- package/src/core/context-discovery.ts +85 -0
- package/src/core/context-refs.ts +140 -0
- package/src/core/gateway.ts +106 -0
- package/src/core/heartbeat.ts +51 -0
- package/src/core/hooks.ts +105 -0
- package/src/core/ide-bridge.ts +133 -0
- package/src/core/node-network.ts +86 -0
- package/src/core/profiles.ts +122 -0
- package/src/core/sandbox.ts +100 -0
- package/src/core/session-manager.ts +137 -0
- package/src/core/vision.ts +180 -0
- package/src/index.ts +84 -1
- package/src/memory/context-compressor.ts +189 -0
- package/src/memory/index.ts +4 -0
- package/src/memory/user-profiler.ts +215 -0
- package/src/security/approvals.ts +143 -0
- package/src/security/elevated.ts +105 -0
- package/src/security/index.ts +6 -0
- package/src/security/secrets.ts +129 -0
- package/src/tools/builtin/browser.ts +299 -0
- package/src/tools/builtin/home-assistant.ts +116 -0
- package/src/tools/builtin/index.ts +9 -2
- package/src/tools/builtin/rl-tools.ts +243 -0
- package/src/tools/builtin/vision.ts +64 -0
- package/tests/api-server.test.ts +148 -0
- package/tests/approvals.test.ts +89 -0
- package/tests/audio.test.ts +40 -0
- package/tests/browser.test.ts +179 -0
- package/tests/builtin-tools.test.ts +83 -83
- package/tests/channels-extra.test.ts +45 -0
- package/tests/context-compressor.test.ts +172 -0
- package/tests/context-refs.test.ts +121 -0
- package/tests/elevated.test.ts +69 -0
- package/tests/gateway.test.ts +63 -71
- package/tests/home-assistant.test.ts +40 -0
- package/tests/hooks.test.ts +79 -0
- package/tests/ide-bridge.test.ts +38 -0
- package/tests/node-network.test.ts +74 -0
- package/tests/profiles.test.ts +61 -0
- package/tests/rl-tools.test.ts +93 -0
- package/tests/sandbox-manager.test.ts +46 -0
- package/tests/secrets.test.ts +107 -0
- package/tests/tools/builtin-extended.test.ts +138 -138
- package/tests/user-profiler.test.ts +169 -0
- package/tests/v090-features.test.ts +254 -0
- package/tests/vision.test.ts +61 -0
- package/tests/voice-call.test.ts +47 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio Processor - v1.0.0
|
|
3
|
+
* Audio transcription/synthesis wrappers with format detection, duration, split.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type AudioFormat = 'wav' | 'mp3' | 'ogg' | 'flac' | 'webm' | 'aac' | 'unknown';
|
|
7
|
+
|
|
8
|
+
export interface TranscribeOptions {
|
|
9
|
+
language?: string;
|
|
10
|
+
model?: string;
|
|
11
|
+
provider?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SynthesizeOptions {
|
|
15
|
+
voice?: string;
|
|
16
|
+
speed?: number;
|
|
17
|
+
format?: AudioFormat;
|
|
18
|
+
provider?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TranscribeResult {
|
|
22
|
+
text: string;
|
|
23
|
+
language?: string;
|
|
24
|
+
duration?: number;
|
|
25
|
+
segments?: { start: number; end: number; text: string }[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SynthesizeResult {
|
|
29
|
+
audio: Buffer;
|
|
30
|
+
format: AudioFormat;
|
|
31
|
+
duration?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type TranscribeFunction = (audio: Buffer, options?: TranscribeOptions) => Promise<TranscribeResult>;
|
|
35
|
+
export type SynthesizeFunction = (text: string, options?: SynthesizeOptions) => Promise<SynthesizeResult>;
|
|
36
|
+
|
|
37
|
+
const FORMAT_SIGNATURES: [Buffer, AudioFormat][] = [
|
|
38
|
+
[Buffer.from('RIFF'), 'wav'],
|
|
39
|
+
[Buffer.from([0xff, 0xfb]), 'mp3'],
|
|
40
|
+
[Buffer.from([0xff, 0xf3]), 'mp3'],
|
|
41
|
+
[Buffer.from([0xff, 0xf2]), 'mp3'],
|
|
42
|
+
[Buffer.from([0x49, 0x44, 0x33]), 'mp3'], // ID3
|
|
43
|
+
[Buffer.from('OggS'), 'ogg'],
|
|
44
|
+
[Buffer.from('fLaC'), 'flac'],
|
|
45
|
+
[Buffer.from([0x1a, 0x45, 0xdf, 0xa3]), 'webm'],
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
export class AudioProcessor {
|
|
49
|
+
private transcribeFn?: TranscribeFunction;
|
|
50
|
+
private synthesizeFn?: SynthesizeFunction;
|
|
51
|
+
|
|
52
|
+
constructor(options?: { transcribe?: TranscribeFunction; synthesize?: SynthesizeFunction }) {
|
|
53
|
+
this.transcribeFn = options?.transcribe;
|
|
54
|
+
this.synthesizeFn = options?.synthesize;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Detect audio format from buffer header */
|
|
58
|
+
static detectFormat(audio: Buffer): AudioFormat {
|
|
59
|
+
for (const [sig, fmt] of FORMAT_SIGNATURES) {
|
|
60
|
+
if (audio.length >= sig.length && audio.subarray(0, sig.length).equals(sig)) return fmt;
|
|
61
|
+
}
|
|
62
|
+
return 'unknown';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Estimate duration in seconds for WAV files (for others returns undefined) */
|
|
66
|
+
static estimateDuration(audio: Buffer): number | undefined {
|
|
67
|
+
const fmt = AudioProcessor.detectFormat(audio);
|
|
68
|
+
if (fmt === 'wav' && audio.length >= 44) {
|
|
69
|
+
const sampleRate = audio.readUInt32LE(24);
|
|
70
|
+
const byteRate = audio.readUInt32LE(28);
|
|
71
|
+
if (byteRate > 0) {
|
|
72
|
+
const dataSize = audio.length - 44;
|
|
73
|
+
return dataSize / byteRate;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Split audio buffer into chunks of roughly `chunkBytes` size */
|
|
80
|
+
static split(audio: Buffer, chunkBytes: number): Buffer[] {
|
|
81
|
+
if (chunkBytes <= 0) throw new Error('chunkBytes must be positive');
|
|
82
|
+
const chunks: Buffer[] = [];
|
|
83
|
+
for (let i = 0; i < audio.length; i += chunkBytes) {
|
|
84
|
+
chunks.push(audio.subarray(i, Math.min(i + chunkBytes, audio.length)));
|
|
85
|
+
}
|
|
86
|
+
return chunks;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async transcribe(audio: Buffer, options?: TranscribeOptions): Promise<TranscribeResult> {
|
|
90
|
+
if (!this.transcribeFn) throw new Error('No transcribe provider configured');
|
|
91
|
+
return this.transcribeFn(audio, options);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async synthesize(text: string, options?: SynthesizeOptions): Promise<SynthesizeResult> {
|
|
95
|
+
if (!this.synthesizeFn) throw new Error('No synthesize provider configured');
|
|
96
|
+
return this.synthesizeFn(text, options);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
export interface ContextFile {
|
|
5
|
+
path: string;
|
|
6
|
+
type: 'agents' | 'soul' | 'user' | 'memory' | 'tools' | 'identity' | 'heartbeat' | 'bootstrap' | 'custom';
|
|
7
|
+
content: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const FILE_TYPE_MAP: Record<string, ContextFile['type']> = {
|
|
11
|
+
'AGENTS.md': 'agents',
|
|
12
|
+
'SOUL.md': 'soul',
|
|
13
|
+
'USER.md': 'user',
|
|
14
|
+
'MEMORY.md': 'memory',
|
|
15
|
+
'TOOLS.md': 'tools',
|
|
16
|
+
'IDENTITY.md': 'identity',
|
|
17
|
+
'HEARTBEAT.md': 'heartbeat',
|
|
18
|
+
'BOOTSTRAP.md': 'bootstrap',
|
|
19
|
+
'.opc.md': 'custom',
|
|
20
|
+
'.opc/config.md': 'custom',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export class ContextDiscovery {
|
|
24
|
+
static STANDARD_FILES = [
|
|
25
|
+
'AGENTS.md', 'SOUL.md', 'USER.md', 'MEMORY.md', 'TOOLS.md',
|
|
26
|
+
'IDENTITY.md', 'HEARTBEAT.md', 'BOOTSTRAP.md',
|
|
27
|
+
'.opc.md', '.opc/config.md',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
private customFiles: string[] = [];
|
|
31
|
+
private watchers: fs.FSWatcher[] = [];
|
|
32
|
+
|
|
33
|
+
discover(workDir?: string): ContextFile[] {
|
|
34
|
+
const dir = workDir || process.cwd();
|
|
35
|
+
const found: ContextFile[] = [];
|
|
36
|
+
|
|
37
|
+
for (const file of ContextDiscovery.STANDARD_FILES) {
|
|
38
|
+
const fullPath = path.join(dir, file);
|
|
39
|
+
if (fs.existsSync(fullPath)) {
|
|
40
|
+
found.push({
|
|
41
|
+
path: fullPath,
|
|
42
|
+
type: FILE_TYPE_MAP[file] || 'custom',
|
|
43
|
+
content: fs.readFileSync(fullPath, 'utf-8'),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const file of this.customFiles) {
|
|
49
|
+
const fullPath = path.isAbsolute(file) ? file : path.join(dir, file);
|
|
50
|
+
if (fs.existsSync(fullPath)) {
|
|
51
|
+
found.push({
|
|
52
|
+
path: fullPath,
|
|
53
|
+
type: 'custom',
|
|
54
|
+
content: fs.readFileSync(fullPath, 'utf-8'),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return found;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
load(files: ContextFile[]): string {
|
|
63
|
+
return files.map(f => `# ${f.type.toUpperCase()}\n${f.content}`).join('\n\n');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
watch(workDir: string, onChange: Function): void {
|
|
67
|
+
const watcher = fs.watch(workDir, { recursive: false }, (event, filename) => {
|
|
68
|
+
if (filename && ContextDiscovery.STANDARD_FILES.includes(filename)) {
|
|
69
|
+
onChange(filename, event);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
this.watchers.push(watcher);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
stopWatching(): void {
|
|
76
|
+
for (const w of this.watchers) w.close();
|
|
77
|
+
this.watchers = [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
addCustomFile(filePath: string): void {
|
|
81
|
+
if (!this.customFiles.includes(filePath)) {
|
|
82
|
+
this.customFiles.push(filePath);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
|
|
5
|
+
export type RefType = 'file' | 'folder' | 'url' | 'git-diff' | 'git-log';
|
|
6
|
+
|
|
7
|
+
export interface ContextRef {
|
|
8
|
+
type: RefType;
|
|
9
|
+
path: string;
|
|
10
|
+
content?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Message {
|
|
14
|
+
role: string;
|
|
15
|
+
content: string;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MAX_CONTENT_LENGTH = 5000;
|
|
20
|
+
|
|
21
|
+
function truncate(text: string, max: number = MAX_CONTENT_LENGTH): string {
|
|
22
|
+
if (text.length <= max) return text;
|
|
23
|
+
return text.slice(0, max) + `\n...[truncated, ${text.length - max} chars omitted]`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class ContextRefResolver {
|
|
27
|
+
/**
|
|
28
|
+
* Parse @-references from text without resolving content.
|
|
29
|
+
*/
|
|
30
|
+
parseRefs(text: string): ContextRef[] {
|
|
31
|
+
const refs: ContextRef[] = [];
|
|
32
|
+
const patterns: [RegExp, RefType, (m: RegExpMatchArray) => string][] = [
|
|
33
|
+
[/@file:(\S+)/g, 'file', (m) => m[1]],
|
|
34
|
+
[/@folder:(\S+)/g, 'folder', (m) => m[1]],
|
|
35
|
+
[/@url:(https?:\/\/\S+)/g, 'url', (m) => m[1]],
|
|
36
|
+
[/@git-diff\b/g, 'git-diff', () => 'git-diff'],
|
|
37
|
+
[/@git-log:(\d+)/g, 'git-log', (m) => m[1]],
|
|
38
|
+
[/@git-log\b(?!:)/g, 'git-log', () => '10'],
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
for (const [regex, type, extract] of patterns) {
|
|
42
|
+
let match: RegExpExecArray | null;
|
|
43
|
+
while ((match = regex.exec(text)) !== null) {
|
|
44
|
+
refs.push({ type, path: extract(match) });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return refs;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve content for each ref. Returns new array with content filled in.
|
|
52
|
+
*/
|
|
53
|
+
async resolveRefs(refs: ContextRef[]): Promise<ContextRef[]> {
|
|
54
|
+
return Promise.all(refs.map(async (ref) => {
|
|
55
|
+
try {
|
|
56
|
+
const content = await this.resolveOne(ref);
|
|
57
|
+
return { ...ref, content: truncate(content) };
|
|
58
|
+
} catch (err) {
|
|
59
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
60
|
+
return { ...ref, content: `[Error resolving @${ref.type}:${ref.path}]: ${msg}` };
|
|
61
|
+
}
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private async resolveOne(ref: ContextRef): Promise<string> {
|
|
66
|
+
switch (ref.type) {
|
|
67
|
+
case 'file':
|
|
68
|
+
return fs.readFileSync(ref.path, 'utf-8');
|
|
69
|
+
case 'folder':
|
|
70
|
+
return this.listDir(ref.path);
|
|
71
|
+
case 'url':
|
|
72
|
+
return await this.fetchUrl(ref.path);
|
|
73
|
+
case 'git-diff':
|
|
74
|
+
return execSync('git diff', { encoding: 'utf-8', timeout: 10000 });
|
|
75
|
+
case 'git-log': {
|
|
76
|
+
const n = parseInt(ref.path) || 10;
|
|
77
|
+
return execSync(`git log --oneline -${n}`, { encoding: 'utf-8', timeout: 10000 });
|
|
78
|
+
}
|
|
79
|
+
default:
|
|
80
|
+
return `[Unknown ref type: ${ref.type}]`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private listDir(dirPath: string, prefix: string = '', depth: number = 0): string {
|
|
85
|
+
if (depth > 5) return prefix + '...(max depth)\n';
|
|
86
|
+
if (!fs.existsSync(dirPath)) throw new Error(`Directory not found: ${dirPath}`);
|
|
87
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
88
|
+
let result = '';
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
91
|
+
result += `${prefix}${entry.isDirectory() ? '📁 ' : '📄 '}${entry.name}\n`;
|
|
92
|
+
if (entry.isDirectory()) {
|
|
93
|
+
result += this.listDir(path.join(dirPath, entry.name), prefix + ' ', depth + 1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private async fetchUrl(url: string): Promise<string> {
|
|
100
|
+
const controller = new AbortController();
|
|
101
|
+
const timeout = setTimeout(() => controller.abort(), 10000);
|
|
102
|
+
try {
|
|
103
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
104
|
+
const text = await res.text();
|
|
105
|
+
return text;
|
|
106
|
+
} finally {
|
|
107
|
+
clearTimeout(timeout);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Inject resolved refs as system messages before the user's last message.
|
|
113
|
+
*/
|
|
114
|
+
injectRefs(messages: Message[], refs: ContextRef[]): Message[] {
|
|
115
|
+
if (refs.length === 0) return messages;
|
|
116
|
+
|
|
117
|
+
const resolvedRefs = refs.filter(r => r.content);
|
|
118
|
+
if (resolvedRefs.length === 0) return messages;
|
|
119
|
+
|
|
120
|
+
const contextMessages: Message[] = resolvedRefs.map(ref => ({
|
|
121
|
+
role: 'system',
|
|
122
|
+
content: `[Context from @${ref.type}:${ref.path}]\n\`\`\`\n${ref.content}\n\`\`\``,
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
// Insert before the last user message
|
|
126
|
+
const result = [...messages];
|
|
127
|
+
let lastUserIdx = -1;
|
|
128
|
+
for (let i = result.length - 1; i >= 0; i--) {
|
|
129
|
+
if (result[i].role === 'user') { lastUserIdx = i; break; }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (lastUserIdx >= 0) {
|
|
133
|
+
result.splice(lastUserIdx, 0, ...contextMessages);
|
|
134
|
+
} else {
|
|
135
|
+
result.push(...contextMessages);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
|
|
4
|
+
export interface AgentConfig {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
model?: string;
|
|
8
|
+
skills?: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ChannelConfig {
|
|
12
|
+
id: string;
|
|
13
|
+
type: string;
|
|
14
|
+
config?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface GatewayConfig {
|
|
18
|
+
port: number;
|
|
19
|
+
agents: AgentConfig[];
|
|
20
|
+
channels: ChannelConfig[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface GatewayMessage {
|
|
24
|
+
id: string;
|
|
25
|
+
content: string;
|
|
26
|
+
channel: string;
|
|
27
|
+
timestamp: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class Gateway extends EventEmitter {
|
|
31
|
+
private config: GatewayConfig;
|
|
32
|
+
private agents = new Map<string, AgentConfig>();
|
|
33
|
+
private channels = new Map<string, ChannelConfig>();
|
|
34
|
+
private running = false;
|
|
35
|
+
private startTime = 0;
|
|
36
|
+
private messagesProcessed = 0;
|
|
37
|
+
private latencies: number[] = [];
|
|
38
|
+
private errors = 0;
|
|
39
|
+
|
|
40
|
+
constructor(config: GatewayConfig) {
|
|
41
|
+
super();
|
|
42
|
+
this.config = config;
|
|
43
|
+
for (const a of config.agents) this.agents.set(a.id, a);
|
|
44
|
+
for (const c of config.channels) this.channels.set(c.id, c);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async start(): Promise<void> {
|
|
48
|
+
if (this.running) throw new Error('Gateway already running');
|
|
49
|
+
this.running = true;
|
|
50
|
+
this.startTime = Date.now();
|
|
51
|
+
this.emit('started');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async stop(): Promise<void> {
|
|
55
|
+
if (!this.running) throw new Error('Gateway not running');
|
|
56
|
+
this.running = false;
|
|
57
|
+
this.emit('stopped');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async routeMessage(message: GatewayMessage, channel: string): Promise<string> {
|
|
61
|
+
if (!this.running) throw new Error('Gateway not running');
|
|
62
|
+
const start = Date.now();
|
|
63
|
+
this.messagesProcessed++;
|
|
64
|
+
// Simple round-robin routing
|
|
65
|
+
const agentIds = Array.from(this.agents.keys());
|
|
66
|
+
if (agentIds.length === 0) {
|
|
67
|
+
this.errors++;
|
|
68
|
+
throw new Error('No agents available');
|
|
69
|
+
}
|
|
70
|
+
const agentId = agentIds[this.messagesProcessed % agentIds.length];
|
|
71
|
+
this.latencies.push(Date.now() - start);
|
|
72
|
+
return agentId;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
addAgent(config: AgentConfig): void {
|
|
76
|
+
this.agents.set(config.id, config);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
removeAgent(id: string): void {
|
|
80
|
+
if (!this.agents.has(id)) throw new Error(`Agent ${id} not found`);
|
|
81
|
+
this.agents.delete(id);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
addChannel(config: ChannelConfig): void {
|
|
85
|
+
this.channels.set(config.id, config);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getStatus(): { uptime: number; agents: number; channels: number; messagesProcessed: number } {
|
|
89
|
+
return {
|
|
90
|
+
uptime: this.running ? Date.now() - this.startTime : 0,
|
|
91
|
+
agents: this.agents.size,
|
|
92
|
+
channels: this.channels.size,
|
|
93
|
+
messagesProcessed: this.messagesProcessed,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getMetrics(): { messagesPerMinute: number; avgLatency: number; errorRate: number } {
|
|
98
|
+
const upMinutes = this.running ? (Date.now() - this.startTime) / 60000 : 1;
|
|
99
|
+
const avgLatency = this.latencies.length ? this.latencies.reduce((a, b) => a + b, 0) / this.latencies.length : 0;
|
|
100
|
+
return {
|
|
101
|
+
messagesPerMinute: this.messagesProcessed / Math.max(upMinutes, 0.001),
|
|
102
|
+
avgLatency,
|
|
103
|
+
errorRate: this.messagesProcessed ? this.errors / this.messagesProcessed : 0,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface HeartbeatConfig {
|
|
2
|
+
interval: number;
|
|
3
|
+
checkFn: () => Promise<string>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export class HeartbeatManager {
|
|
7
|
+
private config: HeartbeatConfig;
|
|
8
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
9
|
+
private callbacks: ((status: string) => void)[] = [];
|
|
10
|
+
private lastBeat: { timestamp: number; status: string } | null = null;
|
|
11
|
+
|
|
12
|
+
constructor(config: HeartbeatConfig) {
|
|
13
|
+
if (!config.interval || config.interval < 100) {
|
|
14
|
+
throw new Error('HeartbeatManager requires interval >= 100ms');
|
|
15
|
+
}
|
|
16
|
+
if (typeof config.checkFn !== 'function') {
|
|
17
|
+
throw new Error('HeartbeatManager requires checkFn');
|
|
18
|
+
}
|
|
19
|
+
this.config = config;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
start(): void {
|
|
23
|
+
if (this.timer) return;
|
|
24
|
+
this.timer = setInterval(async () => {
|
|
25
|
+
try {
|
|
26
|
+
const status = await this.config.checkFn();
|
|
27
|
+
this.lastBeat = { timestamp: Date.now(), status };
|
|
28
|
+
for (const cb of this.callbacks) cb(status);
|
|
29
|
+
} catch (err: any) {
|
|
30
|
+
const status = `error: ${err.message}`;
|
|
31
|
+
this.lastBeat = { timestamp: Date.now(), status };
|
|
32
|
+
for (const cb of this.callbacks) cb(status);
|
|
33
|
+
}
|
|
34
|
+
}, this.config.interval);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
stop(): void {
|
|
38
|
+
if (this.timer) {
|
|
39
|
+
clearInterval(this.timer);
|
|
40
|
+
this.timer = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
onBeat(callback: (status: string) => void): void {
|
|
45
|
+
this.callbacks.push(callback);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getLastBeat(): { timestamp: number; status: string } | null {
|
|
49
|
+
return this.lastBeat;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks Module - v1.0.0
|
|
3
|
+
* Event hook system with priority ordering and context modification.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type HookEvent =
|
|
7
|
+
| 'before:message' | 'after:message'
|
|
8
|
+
| 'before:tool' | 'after:tool'
|
|
9
|
+
| 'before:llm' | 'after:llm'
|
|
10
|
+
| 'before:send' | 'after:send'
|
|
11
|
+
| 'before:learn' | 'after:learn'
|
|
12
|
+
| 'before:recall' | 'after:recall'
|
|
13
|
+
| 'on:error' | 'on:start' | 'on:stop';
|
|
14
|
+
|
|
15
|
+
export const ALL_HOOK_EVENTS: HookEvent[] = [
|
|
16
|
+
'before:message', 'after:message',
|
|
17
|
+
'before:tool', 'after:tool',
|
|
18
|
+
'before:llm', 'after:llm',
|
|
19
|
+
'before:send', 'after:send',
|
|
20
|
+
'before:learn', 'after:learn',
|
|
21
|
+
'before:recall', 'after:recall',
|
|
22
|
+
'on:error', 'on:start', 'on:stop',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export interface HookContext {
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type HookHandler = (ctx: HookContext) => HookContext | void | Promise<HookContext | void>;
|
|
30
|
+
|
|
31
|
+
interface RegisteredHook {
|
|
32
|
+
id: string;
|
|
33
|
+
event: HookEvent;
|
|
34
|
+
handler: HookHandler;
|
|
35
|
+
priority: number;
|
|
36
|
+
name?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let hookIdCounter = 0;
|
|
40
|
+
|
|
41
|
+
export class HookManager {
|
|
42
|
+
private hooks: Map<HookEvent, RegisteredHook[]> = new Map();
|
|
43
|
+
|
|
44
|
+
register(event: HookEvent, handler: HookHandler, options?: { priority?: number; name?: string }): string {
|
|
45
|
+
const id = `hook_${++hookIdCounter}`;
|
|
46
|
+
const entry: RegisteredHook = {
|
|
47
|
+
id,
|
|
48
|
+
event,
|
|
49
|
+
handler,
|
|
50
|
+
priority: options?.priority ?? 100,
|
|
51
|
+
name: options?.name,
|
|
52
|
+
};
|
|
53
|
+
if (!this.hooks.has(event)) this.hooks.set(event, []);
|
|
54
|
+
const list = this.hooks.get(event)!;
|
|
55
|
+
list.push(entry);
|
|
56
|
+
list.sort((a, b) => a.priority - b.priority);
|
|
57
|
+
return id;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
unregister(id: string): boolean {
|
|
61
|
+
for (const [event, list] of this.hooks) {
|
|
62
|
+
const idx = list.findIndex(h => h.id === id);
|
|
63
|
+
if (idx !== -1) {
|
|
64
|
+
list.splice(idx, 1);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async run(event: HookEvent, ctx: HookContext = {}): Promise<HookContext> {
|
|
72
|
+
const list = this.hooks.get(event);
|
|
73
|
+
if (!list || list.length === 0) return ctx;
|
|
74
|
+
let current = { ...ctx };
|
|
75
|
+
for (const hook of list) {
|
|
76
|
+
const result = await hook.handler(current);
|
|
77
|
+
if (result) current = { ...current, ...result };
|
|
78
|
+
}
|
|
79
|
+
return current;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getRegistered(event?: HookEvent): { id: string; event: HookEvent; priority: number; name?: string }[] {
|
|
83
|
+
const results: { id: string; event: HookEvent; priority: number; name?: string }[] = [];
|
|
84
|
+
const events = event ? [event] : ALL_HOOK_EVENTS;
|
|
85
|
+
for (const e of events) {
|
|
86
|
+
const list = this.hooks.get(e) ?? [];
|
|
87
|
+
for (const h of list) {
|
|
88
|
+
results.push({ id: h.id, event: h.event, priority: h.priority, name: h.name });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return results;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
clear(event?: HookEvent): void {
|
|
95
|
+
if (event) {
|
|
96
|
+
this.hooks.delete(event);
|
|
97
|
+
} else {
|
|
98
|
+
this.hooks.clear();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
hasHooks(event: HookEvent): boolean {
|
|
103
|
+
return (this.hooks.get(event)?.length ?? 0) > 0;
|
|
104
|
+
}
|
|
105
|
+
}
|