moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

16
sidecar/package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "waggle-sidecar",
"version": "0.1.0",
"type": "module",
"private": true,
"main": "dist/main.js",
"scripts": {
"build": "tsc",
"start": "node dist/main.js"
},
"dependencies": {
"@waggle/core": "*",
"@waggle/agent": "*",
"@waggle/weaver": "*"
}
}

View File

@@ -0,0 +1,124 @@
import { Orchestrator, type OrchestratorConfig } from '@waggle/agent';
import type { MindDB } from '@waggle/core';
import { createEmbeddingProvider } from '@waggle/core';
export type StreamCallback = (event: StreamEvent) => void;
export interface StreamEvent {
type: 'token' | 'tool_use' | 'tool_result' | 'done' | 'error';
data: unknown;
}
export class AgentSession {
private orchestrator!: Orchestrator;
private initPromise: Promise<void>;
constructor(db: MindDB) {
this.initPromise = this.init(db);
}
private async init(db: MindDB): Promise<void> {
const embeddingProvider = await createEmbeddingProvider({
targetDimensions: 1024,
});
this.orchestrator = new Orchestrator({
db,
embedder: embeddingProvider,
});
}
async sendMessage(
message: string,
apiKey: string,
model: string,
onStream?: StreamCallback,
): Promise<string> {
await this.initPromise;
const systemPrompt = this.orchestrator.buildSystemPrompt();
const tools = this.orchestrator.getTools();
try {
const { default: Anthropic } = await import('@anthropic-ai/sdk');
const client = new Anthropic({ apiKey });
const anthropicTools = tools.map(t => ({
name: t.name,
description: t.description,
input_schema: {
type: 'object' as const,
properties: {},
...t.parameters,
},
}));
let messages: Array<{ role: 'user' | 'assistant'; content: string | Array<unknown> }> = [
{ role: 'user', content: message },
];
let maxTurns = 10;
while (maxTurns-- > 0) {
const response = await client.messages.create({
model,
max_tokens: 4096,
system: systemPrompt,
tools: anthropicTools as never,
messages: messages as never,
});
const toolBlocks = response.content.filter((b: { type: string }) => b.type === 'tool_use');
const textBlocks = response.content.filter((b: { type: string }) => b.type === 'text');
if (toolBlocks.length === 0) {
const text = textBlocks.map((b) => (b as { text: string }).text).join('');
onStream?.({ type: 'done', data: text });
return text;
}
const toolResults: Array<unknown> = [];
for (const block of toolBlocks) {
const tb = block as { id: string; name: string; input: Record<string, unknown> };
onStream?.({ type: 'tool_use', data: { name: tb.name, id: tb.id } });
try {
const result = await this.orchestrator.executeTool(tb.name, tb.input);
onStream?.({ type: 'tool_result', data: { name: tb.name, result } });
toolResults.push({
type: 'tool_result',
tool_use_id: tb.id,
content: result,
});
} catch (err) {
const errMsg = (err as Error).message;
onStream?.({ type: 'tool_result', data: { name: tb.name, error: errMsg } });
toolResults.push({
type: 'tool_result',
tool_use_id: tb.id,
content: `Error: ${errMsg}`,
is_error: true,
});
}
}
messages = [
...messages,
{ role: 'assistant', content: response.content as Array<unknown> },
{ role: 'user', content: toolResults as Array<unknown> },
];
}
return 'Max tool turns reached.';
} catch (err) {
const errMsg = (err as Error).message;
onStream?.({ type: 'error', data: errMsg });
if (errMsg.includes('API key') || errMsg.includes('authentication')) {
return 'Please set your API key in Settings to start chatting.';
}
return `Error: ${errMsg}`;
}
}
getOrchestrator(): Orchestrator {
return this.orchestrator;
}
}

51
sidecar/src/main.ts Normal file
View File

@@ -0,0 +1,51 @@
import { createInterface } from 'readline';
import { MindDB } from '@waggle/core';
import { RpcHandler, type JsonRpcRequest } from './rpc-handler.js';
import { WeaverScheduler } from './weaver-scheduler.js';
import path from 'path';
import os from 'os';
import fs from 'fs';
const mindPath = process.env.WAGGLE_MIND_PATH ??
path.join(os.homedir(), '.waggle', 'default.mind');
// Ensure directory exists
const dir = path.dirname(mindPath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const db = new MindDB(mindPath);
const handler = new RpcHandler(db);
const weaver = new WeaverScheduler(db);
weaver.start();
const rl = createInterface({ input: process.stdin });
rl.on('line', async (line) => {
try {
const request = JSON.parse(line) as JsonRpcRequest;
const response = await handler.handle(request);
process.stdout.write(JSON.stringify(response) + '\n');
} catch (err) {
const errorResponse = {
jsonrpc: '2.0',
error: { code: -32700, message: 'Parse error' },
id: null,
};
process.stdout.write(JSON.stringify(errorResponse) + '\n');
}
});
process.on('SIGTERM', () => {
weaver.stop();
db.close();
process.exit(0);
});
process.on('SIGINT', () => {
weaver.stop();
db.close();
process.exit(0);
});
// Signal ready
process.stderr.write('waggle-sidecar:ready\n');

View File

@@ -0,0 +1,48 @@
export interface McpServerConfig {
id: string;
name: string;
command: string;
args: string[];
env?: Record<string, string>;
enabled?: boolean;
}
export class McpManager {
private servers: Map<string, McpServerConfig>;
constructor() {
this.servers = new Map();
}
addServer(config: McpServerConfig): void {
if (this.servers.has(config.id)) {
throw new Error(`Server "${config.id}" already exists`);
}
this.servers.set(config.id, { enabled: true, ...config });
}
removeServer(id: string): void {
this.servers.delete(id);
}
getServer(id: string): McpServerConfig | undefined {
return this.servers.get(id);
}
listServers(): McpServerConfig[] {
return Array.from(this.servers.values());
}
toJSON(): string {
return JSON.stringify(Array.from(this.servers.values()));
}
static fromJSON(json: string): McpManager {
const manager = new McpManager();
const configs = JSON.parse(json) as McpServerConfig[];
for (const config of configs) {
manager.servers.set(config.id, config);
}
return manager;
}
}

139
sidecar/src/rpc-handler.ts Normal file
View File

@@ -0,0 +1,139 @@
import {
type MindDB,
IdentityLayer,
AwarenessLayer,
FrameStore,
SessionStore,
KnowledgeGraph,
} from '@waggle/core';
import { AgentSession } from './agent-session.js';
import { McpManager, type McpServerConfig } from './mcp-manager.js';
export interface JsonRpcRequest {
jsonrpc: '2.0';
method: string;
params?: Record<string, unknown>;
id: number | string;
}
export interface JsonRpcResponse {
jsonrpc: '2.0';
result?: unknown;
error?: { code: number; message: string; data?: unknown };
id: number | string;
}
interface Settings {
[key: string]: unknown;
model: string;
apiKey: string;
}
export class RpcHandler {
private db: MindDB;
private identity: IdentityLayer;
private awareness: AwarenessLayer;
private frames: FrameStore;
private sessions: SessionStore;
private knowledge: KnowledgeGraph;
private settings: Settings;
private agentSession: AgentSession;
private mcpManager: McpManager;
constructor(db: MindDB) {
this.db = db;
this.identity = new IdentityLayer(db);
this.awareness = new AwarenessLayer(db);
this.frames = new FrameStore(db);
this.sessions = new SessionStore(db);
this.knowledge = new KnowledgeGraph(db);
this.settings = { model: 'claude-sonnet-4-6', apiKey: '' };
this.agentSession = new AgentSession(db);
this.mcpManager = new McpManager();
}
async handle(request: JsonRpcRequest): Promise<JsonRpcResponse> {
try {
const result = await this.dispatch(request.method, request.params ?? {});
return { jsonrpc: '2.0', result, id: request.id };
} catch (err) {
if (err instanceof MethodNotFoundError) {
return {
jsonrpc: '2.0',
error: { code: -32601, message: `Method not found: ${request.method}` },
id: request.id,
};
}
return {
jsonrpc: '2.0',
error: { code: -32000, message: (err as Error).message },
id: request.id,
};
}
}
private async dispatch(method: string, params: Record<string, unknown>): Promise<unknown> {
switch (method) {
case 'ping':
return { status: 'ok' };
case 'mind.getIdentity':
return this.identity.exists() ? this.identity.toContext() : 'No identity set.';
case 'mind.getAwareness':
return this.awareness.toContext();
case 'chat.send': {
const message = params.message as string;
if (!message) throw new Error('message is required');
const streamEvents: unknown[] = [];
const response = await this.agentSession.sendMessage(
message,
this.settings.apiKey,
this.settings.model,
(event) => {
streamEvents.push(event);
},
);
return { response, events: streamEvents };
}
case 'settings.get':
return { ...this.settings };
case 'settings.set': {
const key = params.key as string;
const value = params.value;
if (!(key in this.settings)) {
throw new Error(`Unknown setting: ${key}`);
}
(this.settings as Record<string, unknown>)[key] = value;
return { success: true };
}
case 'mcp.list':
return this.mcpManager.listServers();
case 'mcp.add': {
const config = params as unknown as McpServerConfig;
this.mcpManager.addServer(config);
return { success: true };
}
case 'mcp.remove':
this.mcpManager.removeServer(params.id as string);
return { success: true };
default:
throw new MethodNotFoundError(method);
}
}
}
class MethodNotFoundError extends Error {
constructor(method: string) {
super(`Method not found: ${method}`);
this.name = 'MethodNotFoundError';
}
}

View File

@@ -0,0 +1,71 @@
import fs from 'fs';
import path from 'path';
export interface Skill {
name: string;
description: string;
prompt: string;
model?: string;
tools?: string[];
path: string;
}
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
function parseFrontmatter(content: string): { meta: Record<string, string>; body: string } | null {
const match = content.match(FRONTMATTER_RE);
if (!match) return null;
const meta: Record<string, string> = {};
for (const line of match[1].split('\n')) {
const colonIdx = line.indexOf(':');
if (colonIdx === -1) continue;
const key = line.slice(0, colonIdx).trim();
const value = line.slice(colonIdx + 1).trim();
if (key && value) meta[key] = value;
}
return { meta, body: match[2].trim() };
}
export class SkillLoader {
private directories: string[];
constructor(directories: string[]) {
this.directories = directories;
}
discover(): Skill[] {
const skillMap = new Map<string, Skill>();
for (const dir of this.directories) {
if (!fs.existsSync(dir)) continue;
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const skillMdPath = path.join(dir, entry.name, 'SKILL.md');
if (!fs.existsSync(skillMdPath)) continue;
const content = fs.readFileSync(skillMdPath, 'utf-8');
const parsed = parseFrontmatter(content);
if (!parsed || !parsed.meta.name || !parsed.meta.description) continue;
const skill: Skill = {
name: parsed.meta.name,
description: parsed.meta.description,
prompt: parsed.body,
model: parsed.meta.model,
tools: parsed.meta.tools
? parsed.meta.tools.split(',').map(t => t.trim())
: undefined,
path: skillMdPath,
};
skillMap.set(skill.name, skill);
}
}
return Array.from(skillMap.values());
}
}

View File

@@ -0,0 +1,64 @@
import { type MindDB, FrameStore, SessionStore } from '@waggle/core';
import { MemoryWeaver } from '@waggle/weaver';
export interface WeaverConfig {
consolidationIntervalMs: number;
decayIntervalMs: number;
}
const DEFAULT_CONFIG: WeaverConfig = {
consolidationIntervalMs: 60 * 60 * 1000,
decayIntervalMs: 24 * 60 * 60 * 1000,
};
export class WeaverScheduler {
private weaver: MemoryWeaver;
private sessions: SessionStore;
private timers: NodeJS.Timeout[] = [];
private config: WeaverConfig;
constructor(db: MindDB, config: Partial<WeaverConfig> = {}) {
const frames = new FrameStore(db);
const sessions = new SessionStore(db);
this.sessions = sessions;
this.weaver = new MemoryWeaver(db, frames, sessions);
this.config = { ...DEFAULT_CONFIG, ...config };
}
start(): void {
this.timers.push(
setInterval(() => this.runConsolidation(), this.config.consolidationIntervalMs)
);
this.timers.push(
setInterval(() => this.runDecay(), this.config.decayIntervalMs)
);
process.stderr.write('waggle-weaver:started\n');
}
stop(): void {
for (const timer of this.timers) {
clearInterval(timer);
}
this.timers = [];
}
runConsolidation(): void {
try {
const activeSessions = this.sessions.getActive();
for (const session of activeSessions) {
this.weaver.consolidateGop(session.gop_id);
}
} catch (err) {
process.stderr.write(`waggle-weaver:consolidation-error:${(err as Error).message}\n`);
}
}
runDecay(): void {
try {
this.weaver.decayFrames();
this.weaver.strengthenFrames();
} catch (err) {
process.stderr.write(`waggle-weaver:decay-error:${(err as Error).message}\n`);
}
}
}

16
sidecar/tsconfig.json Normal file
View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"sourceMap": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}