36 Commits

Author SHA1 Message Date
Mikhail Larchanka
3b10003c43 Release - v2.0.0 2026-04-14 19:27:24 +02:00
larchanka
3d0d5b0d27 chore: update package lock 2026-04-14 19:22:14 +02:00
larchanka
ae454123a2 chore: update package lock 2026-04-14 19:21:10 +02:00
larchanka
42f537eb4a chore: update package lock 2026-04-14 13:52:46 +02:00
Mikhail Larchanka
bbf7526940 Merge branch 'main' into release/v2.0.0 2026-04-14 13:43:28 +02:00
dependabot[bot]
6a69da9495 chore(deps): bump vite in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 8.0.2 to 8.0.5
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.5
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-10 19:38:04 +02:00
larchanka
bdd8fee826 fix(orchestrator): Ensure abandoned tasks are marked as failed on error 2026-04-10 16:55:32 +02:00
larchanka
0255123229 Fix stale evaluation and formatting in agent prompts 2026-04-10 16:50:57 +02:00
larchanka
6517c45992 Improve prompts 2026-04-10 16:43:34 +02:00
larchanka
cc08878c09 chore: extend agent node with date and format 2026-04-08 23:32:47 +02:00
larchanka
c354f55a14 fix: ensure file ingest tasks are visible on dashboard immediately 2026-04-05 17:01:51 +02:00
larchanka
3d17545dcd Update dependencies 2026-04-05 16:28:43 +02:00
larchanka
645ef8701e Update dependencies 2026-04-05 16:28:23 +02:00
larchanka
242604675a feat: show agent name on hover in dashboard 2026-04-05 16:28:23 +02:00
larchanka
0360275eea docs: update architecture and capability graph to reflect agent-centric model 2026-04-05 16:28:23 +02:00
larchanka
8e395df9ac refactor: implement Agent-based node architecture and dynamic skill loading 2026-04-05 16:28:23 +02:00
dependabot[bot]
d2bc7f3da2 chore(deps): bump picomatch in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [picomatch](https://github.com/micromatch/picomatch).


Updates `picomatch` from 2.3.1 to 2.3.2
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-05 16:27:17 +02:00
dependabot[bot]
3c85eef2bf chore(deps): bump lodash in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [lodash](https://github.com/lodash/lodash).


Updates `lodash` from 4.17.23 to 4.18.1
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.18.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-05 16:20:18 +02:00
dependabot[bot]
dea80b30a1 chore(deps): bump brace-expansion
Bumps the npm_and_yarn group with 1 update in the / directory: [brace-expansion](https://github.com/juliangruber/brace-expansion).


Updates `brace-expansion` from 1.1.12 to 1.1.13
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.12...v1.1.13)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.13
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-05 16:11:31 +02:00
dependabot[bot]
0f0eb1a024 chore(deps): bump ajv in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [ajv](https://github.com/ajv-validator/ajv).


Updates `ajv` from 6.12.6 to 6.14.0
- [Release notes](https://github.com/ajv-validator/ajv/releases)
- [Commits](https://github.com/ajv-validator/ajv/compare/v6.12.6...v6.14.0)

---
updated-dependencies:
- dependency-name: ajv
  dependency-version: 6.14.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-05 15:58:37 +02:00
dependabot[bot]
28f3b6bba4 chore(deps): bump minimatch in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [minimatch](https://github.com/isaacs/minimatch).


Updates `minimatch` from 3.1.2 to 3.1.5
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-05 15:56:58 +02:00
larchanka
6d3c6f3e21 chore: filter out system heartbeats from intelligence pipeline logs 2026-04-05 15:32:29 +02:00
larchanka
54bc04922f fix: small prompt fixes 2026-04-05 15:32:29 +02:00
larchanka
1e13b2ec32 fix: small prompt fixes 2026-04-05 15:32:29 +02:00
larchanka
99794fec95 fix: include ai_queries in /reminders and add chatId security filtering 2026-04-05 15:32:29 +02:00
larchanka
9820566567 fix: small prompt fixes 2026-04-05 15:32:29 +02:00
larchanka
a1a59a610a Fix: Resolve relative paths in send_file to absolute paths in sandboxDir. 2026-04-05 15:32:29 +02:00
larchanka
56f0c965d0 Fix: Re-added support for local_file_url in send_file for backward compatibility and updated planner prompt to use local_path. 2026-04-05 15:32:29 +02:00
larchanka
6501f232ae fix(telegram): handle potentially undefined extension in file upload logic 2026-04-05 15:32:29 +02:00
larchanka
9d70ecc805 Fix: Reliable Telegram file sending. Renamed local_file_url to local_path, added URL validation in agent, and switched to Stream-based uploads in Telegram adapter. 2026-04-05 15:32:29 +02:00
larchanka
35e32c8e4b Hide scheduled query 2026-04-05 15:32:29 +02:00
larchanka
9e021f2593 Remind only reminders 2026-04-05 15:32:29 +02:00
larchanka
4859ef5de7 fix(orchestrator): deduplicate cron notifications 2026-04-05 15:32:29 +02:00
larchanka
432f2eca05 style(telegram): wrap autonomous task queries in blockquotes for better visibility 2026-04-05 15:32:29 +02:00
larchanka
5cceb959a8 fix(cron): improve cron job stability and visibility by adding immediate Telegram feedback and robust logging 2026-04-05 15:32:29 +02:00
larchanka
9b949f7a3a feat: allow skills starting with underscore to work 2026-03-24 16:16:12 +01:00
22 changed files with 1751 additions and 1172 deletions

View File

@@ -91,7 +91,7 @@ AI-Agent/
### Agent layer
- **Planner**: Listens for `plan.create`; uses Lemonade + Model Router to produce a DAG; validates with `validateGraph`; responds with plan.
- **Executor**: Listens for `plan.execute`; computes ready nodes (parallel batch, concurrency limit); dispatches `node.execute` to `node.service` (model-router, rag-service, critic-agent, tool-host); waits for response by `correlationId`; updates Task Memory; after DAG, optional reflection loop (Critic → REVISE → re-run generation, max 3); aggregates result and completes task.
- **Executor**: Listens for `plan.execute`; traverses the DAG and manages **Autonomous Agent Loops**; provides an agent system prompt and core tools; handles **Dynamic Skill Loading**; after DAG, optional reflection loop.
- **Critic**: Listens for `reflection.evaluate`; uses Lemonade with Critic prompt; returns structured `{ decision: PASS|REVISE, feedback, score }`.
### Service layer
@@ -121,7 +121,7 @@ AI-Agent/
3. Core → Planner: `plan.create` (goal); Planner → Core: plan (DAG).
4. Core → Task Memory: `task.create` (taskId, goal, nodes, edges).
5. Core → Executor: `plan.execute` (taskId, plan, goal).
6. Executor runs DAG: `node.execute` to model-router, rag-service, critic-agent, tool-host; Task Memory updates; optional Critic revision loop.
6. Executor runs DAG: executes specialized **Agents** with instructions; Agents use tools and dynamically call `load_skill`; Task Memory updates; optional Critic revision loop.
7. Executor → Core: response with aggregated result.
8. Core → Telegram Adapter: `telegram.send` (chatId, text).
9. User sees reply in Telegram.

View File

@@ -13,7 +13,7 @@ A multi-process AI platform with type-safe IPC and capability-graph execution. U
## Features
- **Multi-agent pipeline**: Planner → Task Memory → Executor → Critic (optional revision loop)
- **Capability graph (DAG)**: Nodes for `generate_text`, `semantic_search`, `reflect`, `tool`; parallel execution where dependencies allow
- **Capability graph (DAG)**: Nodes for specialized **Agents** and tool-agnostic LLM generation; parallel execution where dependencies allow
- **Type-safe IPC**: JSONL over stdin/stdout with Zod-validated envelopes
- **Conversation Memory**: Short-term memory (last 5 tasks) is injected into the Planner for immediate session context; `/new` resets the session and archives the conversation.
- **Session-Scoped RAG**: Memory searches are session-scoped by default to prevent context leakage after `/new`, with an optional `global` scope.

View File

@@ -47,13 +47,14 @@ Prevents chaotic reasoning loops.
## 4. Capability Graph Pattern
Planner produces a Directed Acyclic Graph (DAG):
Planner produces a Directed Acyclic Graph (DAG) consisting of independent **Agents**:
Example:
semantic_search → sql_query → generate_text → reflect
research_agent → coding_agent → testing_agent → analysis_agent
Executor processes nodes sequentially or parallel when possible.
Nodes are now specialized autonomous agents that can dynamically load instructions (Skills) as needed.
Executor processes these agent nodes sequentially or parallel when possible.
---

View File

@@ -1,43 +1,41 @@
# CAPABILITY GRAPH JSON FORMAT
## Execution Plan Structure
```
```json
{
"taskId": "uuid",
"complexity": "medium",
"reflectionMode": "NORMAL",
"nodes": [
{
"id": "node1",
"type": "semantic_search",
"service": "rag-service",
"id": "research-01",
"type": "agent",
"service": "executor",
"input": {
"query": "scalable API architecture"
"name": "Research Agent",
"instructions": "Search for the latest F1 results using http_search."
}
},
{
"id": "node2",
"id": "summary-01",
"type": "agent",
"service": "executor",
"input": {
"name": "Summary Agent",
"instructions": "Summarize the research from {{research-01}} and load the 'email' skill to prepare a draft."
},
"dependsOn": ["research-01"]
},
{
"id": "node-final",
"type": "generate_text",
"service": "model-router",
"input": {
"modelClass": "medium",
"promptTemplate": "architecture_template",
"dependsOn": ["node1"]
}
},
{
"id": "node3",
"type": "reflect",
"service": "critic-agent",
"input": {
"dependsOn": ["node2"]
"prompt": "Construct final Telegram response: {{summary-01}}",
"system_prompt": "analyzer"
}
}
],
"edges": [
{ "from": "node1", "to": "node2" },
{ "from": "node2", "to": "node3" }
{ "from": "research-01", "to": "summary-01" },
{ "from": "summary-01", "to": "node-final" }
]
}
```
@@ -65,8 +63,9 @@ interface CapabilityNode {
## Node types (model-router / Generator)
- **generate_text** — LLM generation; input: `modelClass`, optional `prompt`, context from dependencies.
- **summarize** — Memory extraction from chat history; input: `chatHistory` (text). Uses dedicated summarizer system prompt. Used by Orchestrator for conversation archiving.
- **agent** — Autonomous LLM loop; input: `name`, `instructions`. High-level strategic node that can use tools and dynamically call `load_skill`.
- **generate_text** — Simple LLM generation; input: `prompt`, context from dependencies. Used for final consolidation.
- **summarize** — Memory extraction from chat history; input: `chatHistory`.
## Graph Rules

View File

@@ -26,8 +26,8 @@ Responsibilities:
Responsibilities:
- Intent analysis
- Capability determination
- Execution graph creation
- Model complexity selection
- Creates an **Agent-based Execution Graph** (DAG)
- Assigns specialized roles to agents (input: `name`, `instructions`)
Input:
- User message
@@ -41,10 +41,10 @@ Output:
### 3. Executor Agent
Responsibilities:
- Execute DAG nodes
- Call services
- Aggregate intermediate results
- Update task memory
- Traverses the DAG and manages **Autonomous Agent Loops**
- Provides core tools (shell, browser, search) to agents
- Handles **Dynamic Skill Loading** via `load_skill` tool
- Aggregates results and updates task memory
---

711
_docs/IMPROVEMENTS.md Normal file
View File

@@ -0,0 +1,711 @@
# ManBot AI Platform - Proposed Improvements
## Overview
This document outlines comprehensive improvements for the ManBot AI platform based on architectural analysis, code quality assessment, and performance considerations.
## Project Analysis Summary
ManBot is a sophisticated multi-process AI platform with type-safe IPC and capability-graph execution. It demonstrates excellent architectural foundation with room for refinement in type safety, performance optimization, and developer experience.
### Current Strengths
- **Multi-agent pipeline**: Well-structured Planner → Task Memory → Executor → Critic flow
- **Type-safe IPC**: Zod-validated envelopes with JSONL communication
- **Capability graph**: DAG-based execution with parallel processing
- **Modular design**: Clear separation between adapters, agents, services, and shared utilities
- **Local-first approach**: No cloud dependencies for core AI functionality
- **Comprehensive feature set**: File processing, reminders, RAG, monitoring dashboard
### Key Metrics from Analysis
- **Test coverage**: 17 test files covering core functionality
- **Type safety concerns**: 396+ instances of `any/unknown` types
- **Import complexity**: 126+ relative imports using `../` patterns
- **Async usage**: 727+ async/await patterns for optimization opportunities
- **Error handling**: 66+ try/catch/throw patterns across services
---
## Proposed Improvements
### 1. Code Organization & Maintainability
#### 1.1 Type Safety Improvements
**Priority**: High
**Impact**: Code reliability and developer experience
**Current Issues:**
- 396+ matches for `any/unknown` types across 41 files
- Missing type definitions in protocol schemas
- Inconsistent error type handling
**Proposed Actions:**
```typescript
// Replace any types with proper interfaces
// Before
const data: any = response.data;
// After
interface ApiResponse<T> {
data: T;
status: number;
message?: string;
}
const data: ApiResponse<ExpectedType> = response.data;
```
**Implementation Steps:**
1. Audit all `any` types in `src/shared/protocol.ts`
2. Create comprehensive interfaces for all API responses
3. Add generic type parameters for reusable components
4. Implement strict error type hierarchy
#### 1.2 Import Path Optimization
**Priority**: High
**Impact**: Code maintainability and refactoring
**Current Issues:**
- 126+ relative imports using `../` patterns
- Deep import paths making refactoring difficult
- Inconsistent import organization
**Proposed Actions:**
```typescript
// tsconfig.json additions
{
"compilerOptions": {
"paths": {
"@shared/*": ["src/shared/*"],
"@agents/*": ["src/agents/*"],
"@services/*": ["src/services/*"],
"@adapters/*": ["src/adapters/*"]
}
}
}
// Replace relative imports
// Before
import { Envelope } from "../shared/protocol.js";
import { BaseProcess } from "../shared/base-process.js";
// After
import { Envelope } from "@shared/protocol.js";
import { BaseProcess } from "@shared/base-process.js";
```
**Implementation Steps:**
1. Configure path mapping in `tsconfig.json`
2. Create barrel exports in major directories
3. Systematically replace relative imports
4. Update build configuration
#### 1.3 Error Handling Standardization
**Priority**: Medium
**Impact**: Debugging and user experience
**Current Issues:**
- Inconsistent error types across services
- Missing error context in some cases
- No centralized error handling patterns
**Proposed Actions:**
```typescript
// Create centralized error types
export class ManBotError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly service: string,
public readonly context?: Record<string, unknown>
) {
super(message);
this.name = 'ManBotError';
}
}
// Service-specific error classes
export class LemonadeError extends ManBotError {
constructor(message: string, context?: Record<string, unknown>) {
super(message, 'LEMONADE_ERROR', 'lemonade-adapter', context);
}
}
```
---
### 2. Performance Optimizations
#### 2.1 Browser Service Enhancement
**Priority**: Medium
**Impact**: Web scraping performance and resource usage
**Current Issues:**
- Each request spawns new browser instance (100-200MB per instance)
- No connection pooling for multiple requests
- Potential memory leaks with browser contexts
**Proposed Actions:**
```typescript
// Browser connection pool
class BrowserPool {
private browsers: Browser[] = [];
private maxBrowsers = 3;
private requestQueue: Array<{
url: string;
resolve: (result: any) => void;
reject: (error: Error) => void;
}> = [];
async executeRequest(url: string): Promise<any> {
return new Promise((resolve, reject) => {
this.requestQueue.push({ url, resolve, reject });
this.processQueue();
});
}
private async processQueue() {
if (this.requestQueue.length === 0) return;
const browser = await this.getAvailableBrowser();
const request = this.requestQueue.shift();
if (request) {
this.executeWithBrowser(browser, request);
}
}
}
```
#### 2.2 Model Management Optimization
**Priority**: Medium
**Impact**: AI model response times and resource utilization
**Current Issues:**
- Cold start delays for large models
- No intelligent preloading based on usage patterns
- Fixed keep-alive durations regardless of usage
**Proposed Actions:**
```typescript
// Smart model manager
class SmartModelManager {
private usagePatterns = new Map<string, {
lastUsed: Date;
frequency: number;
avgResponseTime: number;
}>();
predictModelUsage(modelName: string): number {
const pattern = this.usagePatterns.get(modelName);
if (!pattern) return 0;
const hoursSinceLastUse = (Date.now() - pattern.lastUsed.getTime()) / (1000 * 60 * 60);
const frequencyScore = pattern.frequency / 24; // requests per hour
const recencyScore = Math.max(0, 1 - hoursSinceLastUse / 24);
return frequencyScore * recencyScore;
}
optimizeKeepAlive(modelName: string): string {
const prediction = this.predictModelUsage(modelName);
if (prediction > 0.5) return "2h";
if (prediction > 0.2) return "30m";
return "10m";
}
}
```
#### 2.3 Async Pattern Optimization
**Priority**: Medium
**Impact**: System responsiveness and throughput
**Current Issues:**
- 727+ async/await patterns that could be optimized
- Some sequential operations that could be parallelized
- Missing Promise.all optimizations
**Proposed Actions:**
```typescript
// Before: Sequential operations
async function processFiles(files: string[]) {
const results = [];
for (const file of files) {
const result = await processFile(file);
results.push(result);
}
return results;
}
// After: Parallel operations with batching
async function processFiles(files: string[], batchSize = 5) {
const results = [];
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map(file => processFile(file))
);
results.push(...batchResults);
}
return results;
}
```
---
### 3. Security & Dependencies
#### 3.1 Dependency Security Audit
**Priority**: High
**Impact**: Security posture and vulnerability management
**Proposed Actions:**
```bash
# Security audit commands
npm audit --audit-level moderate
npm audit fix
npm update
# Add to package.json scripts
{
"scripts": {
"security:audit": "npm audit --audit-level moderate",
"security:check": "npm outdated",
"security:fix": "npm audit fix && npm update"
}
}
```
**Implementation Steps:**
1. Run comprehensive dependency audit
2. Update vulnerable packages
3. Implement automated security scanning in CI/CD
4. Create dependency update policy
#### 3.2 Environment Configuration Validation
**Priority**: Medium
**Impact**: Runtime reliability and deployment safety
**Proposed Actions:**
```typescript
// Runtime configuration validation
import { z } from "zod";
const configSchema = z.object({
telegram: z.object({
botToken: z.string().min(1, "Telegram bot token is required"),
allowedUserIds: z.string().optional(),
}),
lemonade: z.object({
baseUrl: z.string().url("Invalid Lemonade base URL"),
timeout: z.number().min(1000, "Timeout must be at least 1 second"),
}),
rag: z.object({
embedModel: z.string().min(1),
dbPath: z.string().min(1),
embeddingDimensions: z.number().positive(),
}),
});
export function validateConfig(config: unknown) {
return configSchema.parse(config);
}
```
#### 3.3 Input Sanitization Enhancement
**Priority**: Medium
**Impact**: Security and data integrity
**Proposed Actions:**
```typescript
// Input sanitization utilities
class InputSanitizer {
static sanitizeString(input: unknown, maxLength = 10000): string {
if (typeof input !== 'string') {
throw new Error('Input must be a string');
}
return input
.trim()
.slice(0, maxLength)
.replace(/[<>]/g, ''); // Basic HTML tag removal
}
static sanitizeFilePath(path: unknown): string {
const cleanPath = this.sanitizeString(path, 255);
// Prevent path traversal
return cleanPath.replace(/\.\./g, '').replace(/^\//, '');
}
static validateTelegramUserId(userId: unknown): number {
const id = Number(userId);
if (!Number.isInteger(id) || id <= 0) {
throw new Error('Invalid Telegram user ID');
}
return id;
}
}
```
---
### 4. Testing & Quality Assurance
#### 4.1 End-to-End Testing Framework
**Priority**: Medium
**Impact**: System reliability and regression prevention
**Proposed Actions:**
```typescript
// E2E test framework
class E2ETestSuite {
async testCompleteWorkflow(userInput: string) {
// 1. Send message to Telegram adapter
const response = await this.sendTelegramMessage(userInput);
// 2. Verify task creation
const task = await this.waitForTaskCreation();
expect(task.goal).toContain(userInput);
// 3. Monitor execution pipeline
const execution = await this.monitorExecution(task.id);
expect(execution.status).toBe('completed');
// 4. Verify response delivery
const finalResponse = await this.waitForTelegramResponse();
expect(finalResponse).toBeDefined();
}
}
```
#### 4.2 Performance Benchmarking
**Priority**: Medium
**Impact**: Performance regression detection
**Proposed Actions:**
```typescript
// Performance benchmarks
describe('Performance Benchmarks', () => {
it('should process simple task within 5 seconds', async () => {
const start = Date.now();
await processSimpleTask('Hello world');
const duration = Date.now() - start;
expect(duration).toBeLessThan(5000);
});
it('should handle concurrent requests efficiently', async () => {
const requests = Array(10).fill(null).map(() =>
processSimpleTask('Concurrent test')
);
const start = Date.now();
await Promise.all(requests);
const duration = Date.now() - start;
// Should be significantly faster than sequential processing
expect(duration).toBeLessThan(15000);
});
});
```
#### 4.3 Coverage Analysis Integration
**Priority**: Low
**Impact**: Code quality visibility
**Proposed Actions:**
```json
// package.json additions
{
"scripts": {
"test:coverage": "vitest run --coverage",
"test:coverage:report": "vitest run --coverage && open coverage/index.html",
"test:watch:coverage": "vitest --coverage --watch"
}
}
```
---
### 5. Architecture Enhancements
#### 5.1 Service Discovery System
**Priority**: Low
**Impact**: System flexibility and scalability
**Proposed Actions:**
```typescript
// Service discovery registry
class ServiceRegistry {
private services = new Map<string, ServiceInfo>();
register(service: ServiceInfo) {
this.services.set(service.name, service);
this.announceService(service);
}
discover(serviceName: string): ServiceInfo | null {
return this.services.get(serviceName) || null;
}
private announceService(service: ServiceInfo) {
// Announce service availability to other components
this.broadcast('service:registered', service);
}
}
interface ServiceInfo {
name: string;
version: string;
endpoint: string;
capabilities: string[];
healthCheck: () => Promise<boolean>;
}
```
#### 5.2 Circuit Breaker Pattern
**Priority**: Low
**Impact**: System resilience and fault tolerance
**Proposed Actions:**
```typescript
// Circuit breaker implementation
class CircuitBreaker {
private failures = 0;
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
private lastFailureTime = 0;
async execute<T>(operation: () => Promise<T>): Promise<T> {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime > this.timeout) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failures = 0;
this.state = 'CLOSED';
}
private onFailure() {
this.failures++;
this.lastFailureTime = Date.now();
if (this.failures >= this.threshold) {
this.state = 'OPEN';
}
}
}
```
#### 5.3 Metrics Collection System
**Priority**: Low
**Impact**: Operational visibility and performance monitoring
**Proposed Actions:**
```typescript
// Metrics collection
class MetricsCollector {
private metrics = new Map<string, Metric>();
increment(name: string, value = 1, tags?: Record<string, string>) {
const metric = this.getOrCreateMetric(name, 'counter');
metric.value += value;
metric.tags = tags;
}
histogram(name: string, value: number, tags?: Record<string, string>) {
const metric = this.getOrCreateMetric(name, 'histogram');
metric.values.push(value);
metric.tags = tags;
}
gauge(name: string, value: number, tags?: Record<string, string>) {
const metric = this.getOrCreateMetric(name, 'gauge');
metric.value = value;
metric.tags = tags;
}
getMetrics(): MetricSnapshot[] {
return Array.from(this.metrics.values()).map(metric => ({
...metric,
timestamp: Date.now(),
}));
}
}
```
---
### 6. Developer Experience Improvements
#### 6.1 Development Server with Hot Reload
**Priority**: Low
**Impact**: Development productivity
**Proposed Actions:**
```typescript
// Development server
class DevServer {
private watchers: Array<() => void> = [];
start() {
// Watch TypeScript files
this.watch('src/**/*.ts', () => {
this.rebuild();
this.restartServices();
});
// Watch configuration files
this.watch('config.json', () => {
this.reloadConfig();
});
}
private async rebuild() {
console.log('🔨 Rebuilding...');
await this.run('npm run build');
console.log('✅ Build complete');
}
private async restartServices() {
console.log('🔄 Restarting services...');
// Restart only changed services
}
}
```
#### 6.2 Enhanced Debugging Tools
**Priority**: Low
**Impact**: Debugging efficiency
**Proposed Actions:**
```typescript
// Debugging utilities
class DebugTools {
static async traceMessageFlow(messageId: string) {
const trace = await this.collectTrace(messageId);
console.table(trace);
}
static async inspectService(serviceName: string) {
const status = await this.getServiceStatus(serviceName);
const metrics = await this.getServiceMetrics(serviceName);
return { status, metrics };
}
static async simulateFailure(serviceName: string, errorType: string) {
// Controlled failure simulation for testing
}
}
```
#### 6.3 CLI Management Commands
**Priority**: Low
**Impact**: Operational efficiency
**Proposed Actions:**
```bash
# CLI commands to implement
manbot start # Start all services
manbot stop # Stop all services
manbot status # Show service status
manbot logs [service] # Show service logs
manbot config validate # Validate configuration
manbot config show # Show current configuration
manbot test e2e # Run end-to-end tests
manbot benchmark # Run performance benchmarks
```
---
## Implementation Roadmap
### Phase 1: High Priority (1-2 weeks)
1. **Type Safety Improvements**
- Audit and replace `any` types
- Create comprehensive interfaces
- Implement strict error types
2. **Import Path Optimization**
- Configure path mapping
- Create barrel exports
- Replace relative imports
3. **Security Audit**
- Run dependency audit
- Update vulnerable packages
- Implement security scanning
### Phase 2: Medium Priority (2-4 weeks)
1. **Performance Optimizations**
- Browser service pooling
- Smart model management
- Async pattern optimization
2. **Testing Enhancement**
- E2E testing framework
- Performance benchmarks
- Coverage analysis
3. **Error Handling Standardization**
- Centralized error types
- Consistent error patterns
- Enhanced error context
### Phase 3: Low Priority (4-8 weeks)
1. **Architecture Enhancements**
- Service discovery
- Circuit breakers
- Metrics collection
2. **Developer Experience**
- Development server
- Debugging tools
- CLI commands
3. **Documentation & Maintenance**
- API documentation
- Architecture diagrams
- Maintenance guides
---
## Success Metrics
### Code Quality Metrics
- **Type Safety**: Reduce `any` usage by 90%
- **Import Complexity**: Reduce relative imports by 80%
- **Test Coverage**: Achieve 85%+ coverage
- **Error Handling**: Standardize error patterns across all services
### Performance Metrics
- **Response Time**: 20% improvement in average task completion
- **Memory Usage**: 30% reduction in browser service memory
- **Concurrency**: Handle 10x more concurrent requests
- **Uptime**: 99.9% service availability
### Developer Experience Metrics
- **Setup Time**: Reduce new developer setup to <15 minutes
- **Debug Time**: 50% reduction in debugging time
- **Build Time**: <5 second incremental builds
- **Documentation**: 100% API coverage
---
## Conclusion
The ManBot platform demonstrates excellent architectural foundation with sophisticated multi-agent capabilities. The proposed improvements focus on enhancing type safety, performance optimization, security hardening, and developer experience while maintaining the platform's core strengths.
Implementing these improvements incrementally will ensure continued platform evolution while maintaining system stability and reliability. The phased approach allows for careful testing and validation of each enhancement before full deployment.
The improvements outlined in this document provide a clear roadmap for elevating ManBot from a functional prototype to a production-ready, enterprise-grade AI platform.

1419
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,7 @@
"better-sqlite3": "^12.6.2",
"ffmpeg-static": "^5.3.0",
"node-cron": "^4.2.1",
"node-telegram-bot-api": "^0.67.0",
"node-telegram-bot-api": "^0.63.0",
"nodejs-whisper": "^0.2.9",
"pino": "^9.5.0",
"playwright": "^1.48.0",
@@ -43,9 +43,9 @@
"@types/node-cron": "^3.0.11",
"@types/node-telegram-bot-api": "^0.64.13",
"@types/turndown": "^5.0.5",
"@vitest/coverage-v8": "^4.0.18",
"tsx": "^4.19.2",
"typescript": "^5.7.2",
"vitest": "^4.0.18",
"@vitest/coverage-v8": "^4.0.18"
"vitest": "^4.0.18"
}
}
}

View File

@@ -8,7 +8,7 @@
import TelegramBot from "node-telegram-bot-api";
import { randomUUID } from "node:crypto";
import { mkdir, createWriteStream } from "node:fs";
import { mkdir, createWriteStream, createReadStream, existsSync } from "node:fs";
import { pipeline } from "node:stream/promises";
import { resolve, extname } from "node:path";
import { PROTOCOL_VERSION } from "../shared/protocol.js";
@@ -126,7 +126,7 @@ const HELP_TEXT = `Commands:
- Cancel a reminder: /cancel_reminder <id>
🛠 Skills:
- Add a new skill: /add_skill <URL_TO_SKILL_MD> (downloads to an underscore-prefixed folder)`;
- Add a new skill: /add_skill <URL_TO_SKILL_MD> (git-ignored by default)`;
/** chatId -> current conversation ID for session grouping */
const conversationIdByChat = new Map<number, string>();
@@ -476,7 +476,7 @@ function main(): void {
createWriteStream(targetPath)
);
await sendToUser(chatId, `✅ Skill added to folder: <code>${folderName}</code>\n\nNotes:\n- The skill is currently <b>disabled</b> (starts with <code>_</code>).\n- Rename the folder to remove the underscore to enable it.\n- Use <code>/help</code> to see available commands.`, undefined, undefined, true);
await sendToUser(chatId, `✅ Skill added and <b>enabled</b> in folder: <code>${folderName}</code>\n\nNotes:\n- The folder starts with <code>_</code> to keep it out of Git (ignored).\n- Use <code>/help</code> to see available commands.`, undefined, undefined, true);
} catch (err) {
console.error("[telegram-adapter] /add_skill error:", err);
await sendToUser(chatId, `❌ Error adding skill: ${err instanceof Error ? err.message : String(err)}`);
@@ -508,34 +508,52 @@ function main(): void {
if (typeof pl.chatId === "number" && typeof pl.localPath === "string") {
(async () => {
try {
const path = pl.localPath;
const caption = pl.caption;
const type = pl.type;
const { localPath: path, caption, type } = pl;
if (!existsSync(path) && !path.startsWith("http")) {
throw new Error(`File not found at path: ${path}`);
}
const sendOptions: any = {
caption: caption ? escapeHtml(caption) : undefined,
parse_mode: "HTML"
};
// Use Stream for local files to ensure multipart/form-data upload
const fileData = (path.startsWith("http://") || path.startsWith("https://"))
? path
: createReadStream(path);
if (type === "photo") {
await bot.sendPhoto(pl.chatId, path, { caption });
await bot.sendPhoto(pl.chatId, fileData, sendOptions);
} else if (type === "audio") {
await bot.sendAudio(pl.chatId, path, { caption });
await bot.sendAudio(pl.chatId, fileData, sendOptions);
} else if (type === "video") {
await bot.sendVideo(pl.chatId, path, { caption });
await bot.sendVideo(pl.chatId, fileData, sendOptions);
} else if (type === "document") {
await bot.sendDocument(pl.chatId, path, { caption });
await bot.sendDocument(pl.chatId, fileData, sendOptions);
} else {
// Auto-detect based on extension if not provided
const ext = extname(path).toLowerCase();
const ext = (extname(path).toLowerCase().split('?')[0]) ?? "";
if ([".jpg", ".jpeg", ".png", ".gif", ".bmp"].includes(ext)) {
await bot.sendPhoto(pl.chatId, path, { caption });
await bot.sendPhoto(pl.chatId, fileData, sendOptions);
} else if ([".mp3", ".wav", ".m4a", ".ogg"].includes(ext)) {
await bot.sendAudio(pl.chatId, path, { caption });
await bot.sendAudio(pl.chatId, fileData, sendOptions);
} else if ([".mp4", ".mov", ".avi", ".mkv"].includes(ext)) {
await bot.sendVideo(pl.chatId, path, { caption });
await bot.sendVideo(pl.chatId, fileData, sendOptions);
} else {
await bot.sendDocument(pl.chatId, path, { caption });
await bot.sendDocument(pl.chatId, fileData, sendOptions);
}
}
} catch (err) {
} catch (err: any) {
console.error("[telegram-adapter] Error sending file:", err);
sendToUser(pl.chatId, `⚠️ Failed to send file: ${err instanceof Error ? err.message : String(err)}`);
let userMsg = err instanceof Error ? err.message : String(err);
if (userMsg.includes("wrong type of the web page content")) {
userMsg = "Telegram could not fetch this URL as a file. Make sure it's a direct link to binary data, or download it to the sandbox first.";
} else if (userMsg.includes("FILE_PART_0_MISSING")) {
userMsg = "Failed to upload file to Telegram. The file might be empty or inaccessible.";
}
sendToUser(pl.chatId, `⚠️ Failed to send file: ${userMsg}`);
}
})();
}

View File

@@ -64,17 +64,32 @@ const SKILL_TOOLS: any[] = [
parameters: {
type: "object",
properties: {
local_file_url: { type: "string", description: "Absolute path to the file in the sandbox" },
local_path: { type: "string", description: "Absolute path to the file in the sandbox. MUST be a local path, not a URL." },
brief_file_description: { type: "string", description: "Brief description or caption for the file" },
chatId: { type: "number", description: "Optional chat ID. If omitted, will use the current chat context." }
},
required: ["local_file_url"]
required: ["local_path"]
}
}
},
{
type: "function",
function: {
name: "load_skill",
description: "Load detailed instructions for a specific skill. Use this if you need more information on how to use a skill from the available list.",
parameters: {
type: "object",
properties: {
skillName: { type: "string", description: "The name of the skill to load" }
},
required: ["skillName"]
}
}
}
];
import { randomUUID } from "node:crypto";
import { resolve, isAbsolute } from "node:path";
import { BaseProcess } from "../shared/base-process.js";
import type { CapabilityGraph, CapabilityNode } from "../shared/graph-utils.js";
import {
@@ -88,6 +103,7 @@ import { responsePayloadSchema } from "../shared/protocol.js";
import { getConfig } from "../shared/config.js";
import { SkillManager } from "../services/skill-manager.js";
import { parseTimeExpression } from "../services/time-parser.js";
import { buildAgentPrompt } from "./prompts/agent-node.js";
const PLAN_EXECUTE = "plan.execute";
const NODE_EXECUTE = "node.execute";
@@ -101,7 +117,8 @@ const TYPE_TO_SERVICE: Record<string, string> = {
generate_text: "model-router",
generate: "model-router",
summarize: "model-router",
skill: "model-router", // skills are handled by generation with custom system prompt
skill: "model-router", // legacy skill support
agent: "model-router", // new agent node support
tool: "tool-host",
semantic_search: "rag-service",
reflect: "critic-agent",
@@ -481,10 +498,11 @@ export class ExecutorAgent extends BaseProcess {
...(Object.keys(context).length > 0 && { context }),
};
// Handle skill nodes by swapping prompt and injecting skill system prompt
if (node.type === "skill") {
// Handle agent/skill nodes by running an LLM loop with tool access
if (node.type === "agent" || node.type === "skill") {
const isAgent = node.type === "agent";
const skillName = (node.input?.skillName ?? node.input?.skill) as string;
let task = (node.input?.task ?? node.input?.prompt ?? "") as string;
let task = (node.input?.task ?? node.input?.prompt ?? node.input?.instructions ?? "") as string;
// Replace placeholders from context if present (e.g. {{nodeId}})
for (const [key, value] of Object.entries(context)) {
@@ -497,26 +515,39 @@ export class ExecutorAgent extends BaseProcess {
}
}
const skillPrompt = this.skillManager.getSkillPrompt(skillName);
if (!skillPrompt) {
reject(new Error(`Skill prompt not found: ${skillName}`));
return;
}
// Active Skill loop
// Active Agent/Skill loop
(async () => {
try {
const messages: any[] = [
{ role: "system", content: skillPrompt },
{ role: "user", content: task }
];
const messages: any[] = [];
if (isAgent) {
const skillsList = this.skillManager.listSkills();
const skillsDescription = skillsList.map(s => `- ${s.name}: ${s.description}`).join("\n");
const agentPrompt = buildAgentPrompt(
(node.input?.name as string) || "Task Agent",
task,
skillsDescription,
new Date().toISOString()
);
messages.push({ role: "system", content: agentPrompt });
messages.push({ role: "user", content: "Proceed with the task." });
} else {
// Legacy skill handling
const skillPrompt = this.skillManager.getSkillPrompt(skillName);
if (!skillPrompt) {
reject(new Error(`Skill prompt not found: ${skillName}`));
return;
}
messages.push({ role: "system", content: skillPrompt });
messages.push({ role: "user", content: task });
}
let turnCount = 0;
while (turnCount < MAX_SKILL_TURNS) {
const genResponse = await this.callModelRouter(taskId, node.id, {
type: "generate_text",
input: {
prompt: task, // Fallback, messages are used if present
prompt: task,
messages,
tools: SKILL_TOOLS
},
@@ -531,35 +562,41 @@ export class ExecutorAgent extends BaseProcess {
// Execute each tool call
for (const tc of result.tool_calls) {
const toolName = tc.function.name;
let args = tc.function.arguments;
if (typeof args === "string") {
try {
args = JSON.parse(args);
} catch (e) {
// fallback to raw string if not JSON
}
} catch (e) { }
}
const toolResult = await this.callTool(taskId, node.id, tc.function.name, args, context);
let toolResult: any;
if (toolName === "load_skill") {
const sn = args.skillName;
const content = this.skillManager.getSkillPrompt(sn);
toolResult = content ? `Skill '${sn}' loaded:\n${content}` : `Skill '${sn}' not found.`;
} else {
toolResult = await this.callTool(taskId, node.id, toolName, args, context);
}
let content = typeof toolResult === "string" ? toolResult : JSON.stringify(toolResult);
// Safety truncation for large web pages or shell outputs to stay within context limits
if (content.length > 30000) {
content = content.substring(0, 30000) + "\n\n...[TRUNCATED DUE TO LENGTH]...";
content = content.substring(0, 30000) + "\n\n...[TRUNCATED]...";
}
messages.push({
role: "tool",
tool_call_id: tc.id,
name: tc.function.name,
name: toolName,
content
});
}
turnCount++;
} else {
// No more tool calls, we are done
resolve(result);
return;
}
}
reject(new Error(`Skill exceeded maximum turns (${MAX_SKILL_TURNS})`));
reject(new Error(`Agent/Skill exceeded maximum turns (${MAX_SKILL_TURNS})`));
} catch (err) {
reject(err);
}
@@ -859,8 +896,11 @@ export class ExecutorAgent extends BaseProcess {
): Promise<unknown> {
const input = node.input ?? {};
const nodeInput = input as Record<string, unknown>;
let localPath = (nodeInput.local_file_url as string) || (nodeInput.path as string);
let localPath = (nodeInput.local_path as string) || (nodeInput.local_file_url as string) || (nodeInput.path as string);
if (localPath && (localPath.startsWith("http://") || localPath.startsWith("https://"))) {
throw new Error(`send_file requires a local path, but received a URL: ${localPath}. Download the file first using shell (curl/wget) if needed.`);
}
let caption = (nodeInput.brief_file_description as string) || (nodeInput.caption as string);
const chatId = (nodeInput.chatId as number) || (context._chatId as number);
@@ -899,8 +939,13 @@ export class ExecutorAgent extends BaseProcess {
if (localPath) localPath = resolveValue(localPath).trim();
if (caption) caption = resolveValue(caption).trim();
// Resolve relative path against sandboxDir
if (localPath && !isAbsolute(localPath) && !localPath.startsWith("http")) {
localPath = resolve(getConfig().toolHost.sandboxDir, localPath);
}
if (!localPath) {
throw new Error("send_file requires local_file_url");
throw new Error("send_file requires local_path (or local_file_url)");
}
if (!chatId) {
throw new Error("send_file requires chatId");

View File

@@ -0,0 +1,57 @@
/**
* System prompt for the unified Agent node.
*/
export const AGENT_NODE_SYSTEM_PROMPT = `
<role>Specialized Task Agent: {{name}}</role>
<context>
Your specific task: {{instructions}}
Available skills (overview):
{{skillsDescription}}
</context>
<current_time>
ONLY USE THIS FOR DATE/TIME
{{currentTime}}
</current_time>
<instructions>
## 1. DYNAMIC SKILL LOADING
You have access to many specialized skills, but you only see their names and descriptions initially.
- If a skill in the list fits your task, you **MUST** call \`load_skill(skillName: "...")\` to get the full instructions and API details for that skill.
- After loading a skill, the system will provide the full instructions in the next turn.
## 2. TOOL ACCESS
You have access to the following core tools:
- **"shell"**: For terminal commands (ls, cat, mkdir, etc.).
- **"http_get"**: For fetching web pages.
- **"http_search"**: For searching the web.
- **"send_file"**: For sharing files with the user.
- **"schedule_reminder"**: For setting cron-based reminders.
## 3. OUTPUT FORMATTING
- Provide your final answer in a clear, concise format.
- IF you were given a specific goal in the context, ensure your output directly addresses it.
- Use Telegram-supported HTML tags for formatting if the output is intended for the user.
</instructions>
<available_tools>
- load_skill(skillName: string)
- shell(command: string)
- http_get(url: string, useBrowser: boolean)
- http_search(query: string)
- send_file(local_path: string, brief_file_description: string, chatId?: number)
- schedule_reminder(time: string, message: string, isAction: boolean)
</available_tools>
MISSION: COMPLETE THE TASK. USE TOOLS. LOAD SKILLS IF NEEDED.
`;
export function buildAgentPrompt(name: string, instructions: string, skillsDescription: string, currentTime: string): string {
return AGENT_NODE_SYSTEM_PROMPT
.replace("{{name}}", name)
.replace("{{instructions}}", instructions)
.replace("{{skillsDescription}}", skillsDescription)
.replace("{{currentTime}}", currentTime);
}

View File

@@ -10,8 +10,6 @@ Your name is \`🧬 ManBot\`. You are a Professional Data Analyst and Assistant.
Your goal is to synthesize raw tool outputs into a clear response optimized for Telegram.
</role>
<current_date_iso>${new Date().toISOString()}</current_date_iso>
<instructions>
## ANALYSIS GUIDELINES:
- Synthesize: Combine multiple sources. Identify patterns or contradictions.
@@ -19,17 +17,19 @@ Your goal is to synthesize raw tool outputs into a clear response optimized for
- Tone: Friendly, direct, and conversational. Avoid "As an AI..." or "Here is the data...".
</instructions>
<format_constraint>
<response_format>
${TELEGRAM_HTML_FORMAT_INSTRUCTION}
Output: Telegram HTML only. NEVER use Markdown (replace with allowed tags or remove). NEVER use raw JSON.
</format_constraint>`;
</response_format>
MISSION: COMPLETE THE TASK. REPLY WITH TELEGRAM HTML FORMAT.`;
/**
* Builds the analyzer prompt.
*/
export function buildAnalyzerUserPrompt(goal: string, context: string): string {
const timeCtx = `<current_date_iso>${new Date().toISOString()}</current_date_iso>\n\n`;
if (!context || !context.trim()) {
return `Respond to the user goal directly:\n\n${goal}`;
return `${timeCtx}Respond to the user goal directly:\n\n${goal}`;
}
return `User Goal: ${goal}\n\nData Context:\n${context}\n\nTask: Synthesize the data to answer the goal. Use Telegram HTML formatting (no markdown, no tables).`;
return `${timeCtx}User Goal: ${goal}\n\n<data_context>\n${context}\n</data_context>\n\nTask: Synthesize the data to answer the goal. Use Telegram HTML formatting (no markdown, no tables).`;
}

View File

@@ -52,9 +52,9 @@ Return ONLY a raw JSON object. No markdown wrappers.
* Builds the critic prompt with injection protection.
*/
export function buildCriticPrompt(goal: string, draftOutput: string): string {
// Basic sanitization to prevent tag-breaking injection
const safeGoal = goal.replace(/<\/?[^>]+(>|$)/g, "");
const safeDraft = draftOutput.replace(/<\/?[^>]+(>|$)/g, "");
// Wrap in CDATA to prevent XML structure breakage while preserving original tags
const safeGoal = `<![CDATA[\n${goal.replace(/\]\]>/g, ']]]]><![CDATA[>')}\n]]>`;
const safeDraft = `<![CDATA[\n${draftOutput.replace(/\]\]>/g, ']]]]><![CDATA[>')}\n]]>`;
return `<audit_request>
<user_goal>

View File

@@ -8,9 +8,9 @@ export const PLANNER_SYSTEM_PROMPT = `<role>Strategic Execution Planner</role>
<logic_gate>
IF you can fulfill the user's goal using ONLY your internal knowledge (e.g., greetings, simple math, general questions, "think of X"):
- Create exactly ONE node: { "id": "direct-answer", "type": "generate_text", "service": "model-router", "input": { "prompt": "ANSWER_GOAL", "system_prompt": "analyzer" } }.
- DO NOT use any tools.
- DO NOT use any agents.
ELSE:
- Proceed with creating a Capability Graph.
- Proceed with creating a Capability Graph consisting of specialized Agents.
</logic_gate>
<file_context_awareness>
@@ -20,38 +20,31 @@ The user's goal may contain pre-processed file content injected by the system:
- "[Audio transcript: ...]" prefix: speech-to-text transcript of a voice/audio message.
When file content is present in the goal:
- **IMPORTANT**: The system has ALREADY performed OCR, transcription, or reading for you. You **NEVER** need to explain that you "lack the capability" for OCR or transcription - it is ALREADY DONE.
- **DO NOT** look for tools (shell, etc.) to read these files. They are provided as part of the instruction.
- Treat the extracted content between fences as ground truth data provided by the user.
- If the content says "Warning: No OCR text extracted" or similar, it simply means the model couldn't find text in that specific file; acknowledge this to the user, but still perform any other requested actions.
- If asked to analyse/summarise/translate the content, use it directly in a generate_text node — no extra tools needed.
- UNLESS explicitly asked for something beyond the provided text (like searching the web about it), do not use tools.
- If asked about a file that was indexed (too large to inline), add a "memory.semantic.search" step first.
- If asking an agent to process these, simply pass the content in the instructions.
- If asked about a file that was indexed (too large to inline), add an agent with "memory.semantic.search" capability first.
</file_context_awareness>
<instructions>
## 1. SKILLS FIRST (ABSOLUTE PRIORITY)
Before using raw tools, scan <available_skills>.
- If a skill matches the goal, you **MUST** use \`type: "skill"\`.
- Manual "shell" or "http" chains are a last resort when no skill fits.
## 1. AGENTS FIRST
Break down complex tasks into specialized Agents. Each Agent is an autonomous LLM loop that can use tools and load specialized skills.
## 2. TOOL CONSTRAINTS
The "tool-host" service supports ONLY these 3 names in the "tool" field:
- **"shell"**: For ALL terminal commands. (Example: \`"tool": "shell", "arguments": { "command": "cat file.txt" }\`)
- **"http_get"**: For rendering a specific URL (Playwright).
- **"http_search"**: For finding information on the web.
- **"send_file"** (service: "core"): For sharing files produced or found in the sandbox with the user via Telegram. (Input: \`local_file_url\`, \`brief_file_description\`).
## 2. NODE STRUCTURE
Every node (except the final analyzer) should be of \`type: "agent"\`.
- \`id\`: Unique identifier.
- \`type\`: "agent".
- \`service\`: "executor".
- \`input\`:
- \`name\`: Descriptive role (e.g., "Research Agent", "Coding Agent").
- \`instructions\`: Specific, detailed task for this agent. Use {{nodeId}} to reference output from previous nodes.
## 3. GRAPH ARCHITECTURE RULES
- **Synthesis**: Every research/tool-heavy plan **MUST** end with a "model-router" node (\`system_prompt: "analyzer"\`).
## 3. SKILL USAGE
Scan <available_skills>. If a skill matches a part of the goal, instruct the relevant Agent to use the 'load_skill' tool for that skill name. Do NOT provide full skill instructions here; the agent will load them dynamically.
## 4. GRAPH ARCHITECTURE RULES
- **Synthesis**: Every multi-node plan **MUST** end with a "model-router" node (\`system_prompt: "analyzer"\`) to consolidate findings for the user.
- **Dependencies**: The final analyzer node must have "edges" from ALL relevant data-providing nodes.
- **Acyclic**: Ensure no circular dependencies.
- **Start Node**: At least one node must have no "from" edges.
## 4. VALIDATION CHECKLIST
- Is the JSON syntax perfect?
- Is every "tool" name valid (not 'ls' or 'google')?
- Are all node IDs unique?
- Does the "to" in edges point to an existing "id"?
</instructions>
<output_format>
@@ -62,71 +55,29 @@ Required complexity levels: "small" | "medium" | "large".
export const PLANNER_FEW_SHOT_EXAMPLES = `
<examples>
## Example: System Operation
User: "create folder 'logs' and list permissions"
## Example: Research and Summarize
User: "Who won the F1 race today and why?"
{
"taskId": "task-sys-01",
"complexity": "small",
"reflectionMode": "OFF",
"nodes": [
{
"id": "op-shell",
"type": "tool",
"service": "tool-host",
"input": {
"tool": "shell",
"arguments": { "command": "mkdir -p logs && ls -ld logs" }
}
}
],
"edges": []
}
## Example: Research Task
User: "who won the F1 race today?"
{
"taskId": "task-f1",
"taskId": "task-f1-01",
"complexity": "medium",
"reflectionMode": "OFF",
"nodes": [
{
"id": "f1-search",
"type": "tool",
"service": "tool-host",
"id": "research-agent",
"type": "agent",
"service": "executor",
"input": {
"tool": "http_search",
"arguments": { "query": "F1 race results today" }
"name": "Research Agent",
"instructions": "Find the results of today's F1 race. Use http_search to get the winner and key race events."
}
},
{
"id": "f1-report",
"type": "generate_text",
"service": "model-router",
"input": {
"prompt": "Identify the winner and summarize the podium based on results.",
"system_prompt": "analyzer"
}
}
],
"edges": [
{ "from": "f1-search", "to": "f1-report" }
]
}
## Example: Deep Research
User: "Deep dive into the current status of the RISC-V ecosystem."
{
"taskId": "task-riscv",
"complexity": "large",
"reflectionMode": "OFF",
"nodes": [
{
"id": "research-eco",
"type": "skill",
"id": "summary-agent",
"type": "agent",
"service": "executor",
"input": {
"skillName": "research",
"task": "Investigate RISC-V hardware, software support, and corporate adoption in 2024. Use search first, then follow key documentation links."
"input": {
"name": "Summary Agent",
"instructions": "Based on the research findings: {{research-agent}}, provide a concise summary of the winner and the main reasons for their victory."
}
},
{
@@ -134,139 +85,65 @@ User: "Deep dive into the current status of the RISC-V ecosystem."
"type": "generate_text",
"service": "model-router",
"input": {
"prompt": "Consolidate the RISC-V research into a comprehensive report.",
"prompt": "Construct the final Telegram message based on: {{summary-agent}}",
"system_prompt": "analyzer"
}
}
],
"edges": [
{ "from": "research-eco", "to": "final-report" }
{ "from": "research-agent", "to": "summary-agent" },
{ "from": "summary-agent", "to": "final-report" }
]
}
## Example: Image with OCR Warning
User: "--- image: receipt.jpg ---\nWarning: No OCR text extracted from the image.\n---"
{
"taskId": "task-img-warn",
"complexity": "small",
"reflectionMode": "OFF",
"nodes": [
{
"id": "direct-answer",
"type": "generate_text",
"service": "model-router",
"input": {
"prompt": "The user provided an image but no text could be extracted. Formulate a polite response asking if they wanted a visual description or if they can send a clearer photo.",
"system_prompt": "analyzer"
}
}
],
"edges": []
}
## Example: Reminder
User: "remind me to drink water in 2 hrs"
## Example: Skill Usage (Reminder)
User: "remind me to check my crypto at 9pm"
{
"taskId": "task-rem-01",
"complexity": "small",
"reflectionMode": "OFF",
"nodes": [
{
"id": "rem-node",
"type": "skill",
"id": "rem-agent",
"type": "agent",
"service": "executor",
"input": {
"skillName": "reminder",
"task": "remind me to drink water in 2 hrs"
"name": "Scheduler Agent",
"instructions": "Use the 'load_skill' tool for 'reminder' to see how to schedule this: 'remind me to check my crypto at 9pm'"
}
}
],
"edges": []
}
## Example: Generate and Save
User: "think of a 3-day workout plan and save it to my notes"
## Example: Complex Coding/File Task
User: "Create a python script that fetches btc price and save it to btc.py"
{
"taskId": "task-workout",
"taskId": "task-btc-01",
"complexity": "medium",
"reflectionMode": "OFF",
"nodes": [
{
"id": "gen-plan",
"id": "coder-agent",
"type": "agent",
"service": "executor",
"input": {
"name": "Python Developer",
"instructions": "Write a python script that uses an public API to fetch the current BTC price. Save the code to 'btc.py' using the shell tool."
}
},
{
"id": "final-report",
"type": "generate_text",
"service": "model-router",
"input": { "prompt": "Create a 3-day workout plan for a beginner." }
},
{
"id": "save-notes",
"type": "skill",
"service": "executor",
"input": {
"skillName": "apple-notes",
"task": "Save this workout plan to my notes: {{gen-plan}}"
"input": {
"prompt": "Tell the user that the script btc.py has been created successfully. Mention the code: {{coder-agent}}",
"system_prompt": "analyzer"
}
}
],
"edges": [
{ "from": "gen-plan", "to": "save-notes" }
]
}
## Example: Email/Calendar
User: "check my inbox for unread messages"
{
"taskId": "task-gog-01",
"complexity": "small",
"reflectionMode": "OFF",
"nodes": [
{
"id": "check-mail",
"type": "skill",
"service": "executor",
"input": {
"skillName": "gog",
"task": "check my inbox for unread messages"
}
}
],
"edges": []
}
## Example: Generate and Send File
User: "search for the latest price of Gold and create a simple text report, then send it to me"
{
"taskId": "task-gold-01",
"complexity": "medium",
"reflectionMode": "OFF",
"nodes": [
{
"id": "gold-search",
"type": "tool",
"service": "tool-host",
"input": {
"tool": "http_search",
"arguments": { "query": "current gold price" }
}
},
{
"id": "create-report",
"type": "tool",
"service": "tool-host",
"input": {
"tool": "shell",
"arguments": { "command": "echo \\"Latest Gold Price: $(grep -oE '[0-9,]+\\.[0-9]+' gold_results.txt | head -1)\\" > gold_report.txt && realpath gold_report.txt" }
}
},
{
"id": "send-report",
"type": "send_file",
"service": "core",
"input": {
"local_file_url": "{{create-report}}",
"brief_file_description": "Here is the gold price report you requested."
}
}
],
"edges": [
{ "from": "gold-search", "to": "create-report" },
{ "from": "create-report", "to": "send-report" }
{ "from": "coder-agent", "to": "final-report" }
]
}
</examples>`;
@@ -304,7 +181,16 @@ ${Object.entries(process.env)
"service": "executor",
"input": { "skillName": "NAME", "task": "INSTRUCTION" }
}
</skill_node_template>`;
</skill_node_template>
<agent_node_template>
{
"id": "agent-node",
"type": "agent",
"service": "executor",
"input": { "name": "ROLE_NAME", "instructions": "DETAILED_INSTRUCTIONS" }
}
</agent_node_template>`;
}
const now = new Date().toISOString().split('T')[0];

View File

@@ -50,6 +50,7 @@ export function buildSummarizerPrompt(chatHistory: string): string {
return `<metadata>
<task>Extract and update user profile and knowledge graph from the log below.</task>
<timestamp>${new Date().toISOString()}</timestamp>
</metadata>
<conversation_log>

View File

@@ -7,9 +7,9 @@
export const TELEGRAM_HTML_FORMAT_INSTRUCTION = `## TELEGRAM HTML FORMATTING RULES:
You MUST format your output using Telegram-supported HTML tags. Do NOT use Markdown syntax.
1. **No Markdown**: Do NOT use *, **, _, ~~, \`, #, or any Markdown syntax. Use HTML tags only.
2. **No Tables**: HTML tables are not supported by Telegram. Use structured bullet points (•) or bold lists.
3. **Supported HTML tags**:
1. NO Markdown: Do NOT use *, **, _, ~~, \`, #, or any Markdown syntax. Use HTML tags only.
2. NO Tables: HTML tables are not supported by Telegram. Use structured bullet points (•) or bold lists.
3. Supported HTML tags:
- Bold: <b>text</b>
- Italic: <i>text</i>
- Underline: <u>text</u>
@@ -21,8 +21,8 @@ You MUST format your output using Telegram-supported HTML tags. Do NOT use Markd
- Block quote: <blockquote>quote</blockquote>
- Expandable Block quote (for long quotes): <blockquote expandable>quote</blockquote>
- Code block with language: <pre><code class="language-python">code</code></pre>
4. **Special characters**: The characters <, > and & must be replaced with &lt;, &gt; and &amp; respectively when used as literals (not as part of HTML tags).
5. **Line breaks**: Use regular line breaks (newlines). Do NOT use <br> tags.`;
4. Special characters: The characters <, > and & must be replaced with &lt;, &gt; and &amp; respectively when used as literals (not as part of HTML tags).
5. Line breaks: Use regular line breaks (newlines). Do NOT use <br> tags.`;
/**
* Default system prompt for LLM calls that need Telegram HTML formatting

View File

@@ -191,22 +191,28 @@ export class Orchestrator {
return;
}
// Handle cron AI query events from cron-manager
if (fromProcess === "cron-manager" && envelope.type === "event.cron.ai_query") {
this.handleCronAIQueryEvent(envelope);
return;
}
if (fromProcess === "cron-manager") {
if (envelope.type === "event.cron.ai_query") {
this.handleCronAIQueryEvent(envelope);
} else if (envelope.type === "event.cron.completed") {
const pl = envelope.payload as Record<string, unknown>;
if (pl.taskType === "reminder") {
this.handleCronReminderEvent(envelope);
}
}
// Handle cron reminder events from cron-manager
if (fromProcess === "cron-manager" && envelope.type === "event.cron.completed") {
this.handleCronReminderEvent(envelope);
// Also forward to logger as before
// Always forward cron events to logger for audit trail
const logger = this.children.get("logger");
if (logger?.stdin.writable) {
logger.stdin.write(trimmed + "\n");
ConsoleLogger.ipc("core", "→", envelope);
this.broadcastIpcLog("→", "core", "logger", envelope);
}
return;
// If it was one of our handled events, we're done
if (envelope.type === "event.cron.ai_query" || envelope.type === "event.cron.completed") {
return;
}
}
if (to === "core") {
@@ -559,6 +565,9 @@ export class Orchestrator {
if (details.originalErrorMessage != null) parts.push(String(details.originalErrorMessage));
lastError = parts.join(" ");
}
this.sendAndWait(taskMemory, "task.fail", { taskId, reason: lastError }).catch(() => { });
if (attempt === Orchestrator.MAX_PLAN_RETRIES) {
this.sendToTelegram(chatId, lastError);
return;
@@ -570,6 +579,9 @@ export class Orchestrator {
const plan = planPayload.result as { nodes: unknown[]; edges?: unknown[]; complexity?: string } | undefined;
if (!plan?.nodes || !Array.isArray(plan.nodes)) {
lastError = "Invalid plan from planner: missing or invalid nodes.";
this.sendAndWait(taskMemory, "task.fail", { taskId, reason: lastError }).catch(() => { });
if (attempt === Orchestrator.MAX_PLAN_RETRIES) {
this.sendToTelegram(chatId, lastError);
return;
@@ -580,7 +592,7 @@ export class Orchestrator {
previousPlan = plan;
const nodes = plan.nodes as Array<{ id: string; type: string; service: string; input?: unknown }>;
const edges = (plan.edges ?? []) as Array<{ from: string; to: string }>;
// Update task DAG in memory
await this.sendAndWait(taskMemory, "task.updateDag", {
taskId,
@@ -592,7 +604,7 @@ export class Orchestrator {
}).catch(() => { });
this.sendToTelegram(chatId, "💨 Planning complete. Execution started...", true);
// Explicitly set task to 'running' before dispatching to executor
await this.sendAndWait(taskMemory, "task.updateStatus", { taskId, status: "running" }).catch(() => { });
@@ -610,6 +622,9 @@ export class Orchestrator {
if (details.originalErrorMessage != null) parts.push(String(details.originalErrorMessage));
lastError = parts.join(". ");
}
this.sendAndWait(taskMemory, "task.fail", { taskId, reason: lastError }).catch(() => { });
if (attempt === Orchestrator.MAX_PLAN_RETRIES) {
this.sendToTelegram(chatId, lastError);
return;
@@ -676,6 +691,30 @@ export class Orchestrator {
return;
}
const taskId = randomUUID();
// Create placeholder task in memory so it shows up in dashboard during processing
const taskMemory = this.children.get("task-memory");
if (taskMemory?.stdin.writable) {
this.send({
id: randomUUID(),
timestamp: Date.now(),
from: "core",
to: "task-memory",
type: "task.create",
version: "1.0",
payload: {
taskId,
userId: String(userId),
conversationId: conversationId ?? String(chatId),
goal: caption || "Processing uploaded files...",
status: "pending",
complexity: "unknown",
nodes: [],
edges: []
}
});
}
// Notify user processing has started
const fileWord = files.length === 1 ? "file" : "files";
this.sendToTelegram(chatId, `⏳ Processing ${files.length} ${fileWord}...`, true);
@@ -793,7 +832,7 @@ export class Orchestrator {
// Run the task pipeline with the enriched goal
ConsoleLogger.debug("core", `Running task pipeline with enriched goal (length: ${enrichedGoal.length})`);
await this.runTaskPipeline(chatId, userId, enrichedGoal, conversationId);
await this.runTaskPipeline(chatId, userId, enrichedGoal, conversationId, taskId);
}
// ---------------------------------------------------------------------------
@@ -999,6 +1038,9 @@ export class Orchestrator {
ConsoleLogger.info("core", `Triggering autonomous AI task: "${query}" for chatId ${chatIdNum} (taskId: ${taskId})`);
// TODO: Immediate feedback so user knows the cron triggered
// this.sendToTelegram(chatIdNum, `🤖 <b>Autonomous task triggered</b>:\n<blockquote>${query}</blockquote>\n\nStarting planning...`, true, "HTML");
// Route to task queue (priority 0 for synthetic)
this.enqueueTask({
chatId: chatIdNum,
@@ -1028,30 +1070,48 @@ export class Orchestrator {
id: string;
cronExpr: string;
taskType: string;
payload: string;
enabled: boolean;
}>;
ConsoleLogger.info("core", `Found ${schedules.length} total schedules`);
// Filter reminders for this chatId - we need to check the payload of each schedule
// Since we can't easily query by chatId, we'll need to get schedule details
// For now, filter by taskType === "reminder"
const reminderSchedules = schedules.filter((s) => s.taskType === "reminder" && s.enabled);
// Filter reminders for this chatId
const filteredSchedules = schedules.filter((s) => {
if (!s.enabled) return false;
if (s.taskType !== "reminder" && s.taskType !== "ai_query") return false;
ConsoleLogger.info("core", `Found ${reminderSchedules.length} reminder schedules`);
try {
const payload = JSON.parse(s.payload);
return payload.chatId === chatId;
} catch (err) {
return false;
}
});
if (reminderSchedules.length === 0) {
ConsoleLogger.info("core", `Found ${filteredSchedules.length} filtered schedules for chatId ${chatId}`);
if (filteredSchedules.length === 0) {
ConsoleLogger.info("core", "No reminders found, sending 'No active reminders' message");
this.sendToTelegram(chatId, "👌🏻 No active reminders.");
return;
}
// Format reminders - we'll show ID and cronExpr, but reminderMessage is in the payload
// For a better implementation, we'd need to query each schedule's payload
const formatted = reminderSchedules
.map((rem) => `ID: ${rem.id}\nTime: ${rem.cronExpr}`)
// Format reminders - include message/query and task type
const formatted = filteredSchedules
.map((s) => {
let message = "N/A";
try {
const p = JSON.parse(s.payload);
message = p.reminderMessage || p.query || "N/A";
} catch (e) { }
const typeLabel = s.taskType === "ai_query" ? "🤖 Task" : "🔔 Reminder";
return `<b>${typeLabel}</b>\nID: <code>${s.id}</code>\nTime: <code>${s.cronExpr}</code>\n<blockquote>Message: ${message.substring(0, 100)}...</blockquote>`;
})
.join("\n\n---\n\n");
const message = `Active reminders:\n\n${formatted}`;
const message = `⏰ <b>Active schedules:</b>\n\n${formatted}`;
ConsoleLogger.info("core", `Sending reminder list to chatId ${chatId}: ${message.substring(0, 100)}...`);
this.sendToTelegram(chatId, message, false, "HTML");
} catch (err) {
@@ -1069,12 +1129,37 @@ export class Orchestrator {
}
try {
// Security: verify that this reminder belongs to this chatId
const listResponse = await this.sendAndWait(cronManager, "cron.schedule.list", {});
const listPayload = listResponse.payload as { result?: { schedules?: Array<{ id: string; payload: string }> } };
const schedules = listPayload.result?.schedules ?? [];
const targetSchedule = schedules.find(s => s.id === reminderId);
if (!targetSchedule) {
this.sendToTelegram(chatId, `😨 Reminder <code>${reminderId}</code> not found.`);
return;
}
try {
const payload = JSON.parse(targetSchedule.payload);
if (payload.chatId !== chatId) {
this.sendToTelegram(chatId, `❌ You are not authorized to cancel this reminder.`);
return;
}
} catch (e) {
// If payload is malformed or missing chatId, we might want to prevent deletion or allow it?
// Let's be conservative: if we can't confirm ownership, we reject.
// UNLESS we are superuser (could add later).
this.sendToTelegram(chatId, `❌ Security check failed for reminder <code>${reminderId}</code>.`);
return;
}
const response = await this.sendAndWait(cronManager, "cron.schedule.remove", { id: reminderId });
const responsePayload = response.payload as { status?: string; result?: { removed?: string } };
if (responsePayload.result?.removed === reminderId) {
this.sendToTelegram(chatId, `🟢 Reminder ${reminderId} has been canceled.`);
this.sendToTelegram(chatId, `🟢 Reminder <code>${reminderId}</code> has been canceled.`);
} else {
this.sendToTelegram(chatId, `😨 Reminder ${reminderId} not found.`);
this.sendToTelegram(chatId, `😨 Reminder <code>${reminderId}</code> not found.`);
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);

View File

@@ -72,27 +72,15 @@ export class CronManager extends BaseProcess {
private runJob(row: ScheduleRow): void {
const now = Date.now();
this.emitEvent("event.cron.started", { scheduleId: row.id, taskType: row.task_type, timestamp: now });
this.emitEvent("event.cron.started", {
scheduleId: row.id,
taskType: row.task_type,
timestamp: now,
});
try {
const payload = row.payload ? JSON.parse(row.payload) : {};
// AI Query task type
if (row.task_type === "ai_query") {
const query = payload.reminderMessage || payload.query || "";
const chatId = payload.chatId;
const userId = payload.userId;
this.emitEvent("event.cron.ai_query", {
scheduleId: row.id,
taskType: row.task_type,
query,
chatId,
userId,
timestamp: Date.now()
});
return;
}
// Default: Reminder or generic task payload
const reminderPayload: Record<string, unknown> = {
scheduleId: row.id,
@@ -101,13 +89,25 @@ export class CronManager extends BaseProcess {
timestamp: Date.now(),
};
// If this is a reminder task, extract structured fields
if (row.task_type === "reminder" || payload.chatId || payload.reminderMessage) {
reminderPayload.chatId = payload.chatId;
reminderPayload.reminderMessage = payload.reminderMessage;
reminderPayload.userId = payload.userId;
// Extract common fields for easier routing in Orchestrator if present
const q = payload.reminderMessage || payload.query;
if (q) reminderPayload.reminderMessage = q;
if (payload.chatId !== undefined) reminderPayload.chatId = payload.chatId;
if (payload.userId !== undefined) reminderPayload.userId = payload.userId;
// Emit specific event types for legacy/orchestrator compatibility
if (row.task_type === "ai_query") {
this.emitEvent("event.cron.ai_query", {
scheduleId: row.id,
taskType: row.task_type,
query: payload.reminderMessage || payload.query || "",
chatId: payload.chatId,
userId: payload.userId,
timestamp: Date.now(),
});
}
// Always emit completed event for tracking and logging
this.emitEvent("event.cron.completed", reminderPayload);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
@@ -148,17 +148,19 @@ export class CronManager extends BaseProcess {
return id;
}
private listSchedules(): Array<{ id: string; cronExpr: string; taskType: string; enabled: boolean }> {
const rows = this.db.prepare("SELECT id, cron_expr, task_type, enabled FROM cron_schedules").all() as Array<{
private listSchedules(): Array<{ id: string; cronExpr: string; taskType: string; payload: string; enabled: boolean }> {
const rows = this.db.prepare("SELECT id, cron_expr, task_type, payload, enabled FROM cron_schedules").all() as Array<{
id: string;
cron_expr: string;
task_type: string;
payload: string;
enabled: number;
}>;
return rows.map((r) => ({
id: r.id,
cronExpr: r.cron_expr,
taskType: r.task_type,
payload: r.payload,
enabled: r.enabled === 1,
}));
}

View File

@@ -252,7 +252,7 @@ export class DashboardService extends BaseProcess {
if (fs.existsSync(logPath)) {
stats.logs = fs.readFileSync(logPath, 'utf8').trim().split('\n').map(line => {
try { return JSON.parse(line); } catch (e) { return { message: line }; }
}).reverse();
}).filter((l: any) => l.type !== "event.system.heartbeat").reverse();
}
} catch (e) { }
return stats;

View File

@@ -266,11 +266,16 @@ async function updateDashboard() {
const activeIndicator = ['planning', 'running'].includes(n.status) ? '<div class="pulse"></div>' : '';
const typeLabel = n.type.split('.').pop().toUpperCase();
let chipAttr = '';
if (n.type === 'skill' && n.input) {
if ((n.type === 'skill' || n.type === 'agent') && n.input) {
try {
const input = typeof n.input === 'string' ? JSON.parse(n.input) : n.input;
const skillName = input.skillName || input.skill;
if (skillName) chipAttr = ` data-title="Skill: ${skillName}"`;
if (n.type === 'skill') {
const skillName = input.skillName || input.skill;
if (skillName) chipAttr = ` data-title="Skill: ${skillName}"`;
} else if (n.type === 'agent') {
const agentName = input.name || "Task Agent";
chipAttr = ` data-title="Agent: ${agentName}"`;
}
} catch (e) { }
}
return `<div class="node-chip ${n.status}"${chipAttr}>${activeIndicator}${typeLabel}</div>`;

View File

@@ -57,7 +57,7 @@ export class LoggerService extends BaseProcess {
}
protected override handleEnvelope(envelope: Envelope): void {
if (!envelope.type.startsWith("event.")) {
if (!envelope.type.startsWith("event.") || envelope.type === "event.system.heartbeat") {
return;
}

View File

@@ -31,8 +31,8 @@ export class SkillManager {
const skills: SkillInfo[] = [];
for (const entry of entries) {
// Ignore hidden directories and those starting with underscore (disabled)
if (entry.isDirectory() && !entry.name.startsWith(".") && !entry.name.startsWith("_")) {
// Ignore hidden directories
if (entry.isDirectory() && !entry.name.startsWith(".")) {
const skillMdPath = join(this.skillsDir, entry.name, "SKILL.md");
if (existsSync(skillMdPath)) {
try {
@@ -40,12 +40,12 @@ export class SkillManager {
const lines = content.split("\n").filter(l => l.trim() !== "");
const firstLine = lines[0]?.trim();
if (!firstLine) continue;
let description = firstLine;
if (firstLine.toLowerCase().startsWith("description:")) {
description = firstLine.substring("description:".length).trim();
}
if (description) {
skills.push({
name: entry.name,
@@ -76,7 +76,7 @@ export class SkillManager {
try {
const content = readFileSync(skillPath, "utf-8");
return `${content}\n\n## OUTPUT FORMATTING\n${TELEGRAM_HTML_FORMAT_INSTRUCTION}\n\nYou MUST format your final response using only the Telegram HTML tags listed above. Never use Markdown (replace will allowed HTML tags).`;
return `${content}\n\n## OUTPUT FORMATTING\nYou MUST format your final response using only the Telegram HTML tags listed above. Never use Markdown (replace will allowed HTML tags).\n\n${TELEGRAM_HTML_FORMAT_INSTRUCTION}`;
} catch (err) {
console.error(`Failed to load skill prompt for ${name}:`, err);
return null;