diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4c8fa85 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +node_modules +.git +.gitignore +*.md +!README.md +.env +.DS_Store +logs/ +*.log +tests/ +coverage/ +.nyc_output/ diff --git a/.env.example b/.env.example index 65261d3..3afbc01 100644 --- a/.env.example +++ b/.env.example @@ -62,3 +62,22 @@ RUN_HISTORY_STORAGE=file RUN_HISTORY_FILE=./data/run-history.json # Max number of recent runs to keep RUN_HISTORY_MAX_RUNS=200 + +# ============================================ +# Environment Variables Reference (Stellar Wave #43) +# ============================================ +# | Variable | Required | Default | Description | +# |-----------------------|----------|-------------------|--------------------------------------| +# | PORT | No | 3000 | Server port | +# | NODE_ENV | No | development | Environment (development/production) | +# | LOG_LEVEL | No | info | Logging level (debug/info/warn/error)| +# | API_KEY | Yes | — | Master API key for auth | +# | JWT_SECRET | Yes | — | Secret for JWT token signing | +# | PROVIDER_OPENAI_KEY | No | — | OpenAI API key | +# | PROVIDER_ANTHROPIC_KEY| No | — | Anthropic API key | +# | RATE_LIMIT_WINDOW_MS | No | 60000 | Rate limit window in ms | +# | RATE_LIMIT_MAX | No | 100 | Max requests per window | +# | AUDIT_DIR | No | logs/audit | Audit log directory | +# | AUDIT_RETENTION_DAYS | No | 90 | Audit retention period (days) | +# | REDIS_URL | No | — | Redis connection URL (optional) | +# ============================================ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..e67e2f7 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,14 @@ +# Code Owners +# Automatically request reviews from the right people + +# Core infrastructure +* @Flamki + +# Documentation +*.md @Flamki +docs/ @Flamki + +# CI/CD workflows +.github/workflows/ @Flamki + +# Generated for Stellar Wave bounty #41 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..d3fab89 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,36 @@ +--- +name: Bug Report +about: Report a bug to help improve StellarMind +title: '[BUG] ' +labels: ['bug', 'triage'] +assignees: [] +--- + +### Description +A clear and concise description of the bug. + +### Steps to Reproduce +1. Go to '...' +2. Click on '...' +3. Scroll to '...' +4. See error + +### Expected Behavior +What you expected to happen. + +### Actual Behavior +What actually happened. + +### Screenshots +If applicable, add screenshots. + +### Environment +- OS: [e.g. Windows, macOS, Linux] +- Browser: [e.g. Chrome, Firefox] +- Node.js version: [e.g. 20.11] +- StellarMind version: [e.g. 1.0.0] + +### Additional Context +Add any other context about the problem here. + +_Generated for Stellar Wave bounty #32_ diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..ee9721f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,27 @@ +--- +name: Feature Request +about: Suggest a new feature for StellarMind +title: '[FEATURE] ' +labels: ['enhancement', 'triage'] +assignees: [] +--- + +### Problem Statement +A clear description of the problem this feature would solve. + +### Proposed Solution +Describe the solution you'd like. + +### Alternatives Considered +Describe any alternative solutions you've considered. + +### Use Case +Who would benefit from this feature and how? + +### Implementation Ideas +Any initial thoughts on how this could be implemented. + +### Additional Context +Add any other context or screenshots about the feature request. + +_Generated for Stellar Wave bounty #32_ diff --git a/.versionrc b/.versionrc new file mode 100644 index 0000000..4648664 --- /dev/null +++ b/.versionrc @@ -0,0 +1,17 @@ +{ + "types": [ + {"type": "feat", "section": "Features"}, + {"type": "fix", "section": "Bug Fixes"}, + {"type": "docs", "section": "Documentation"}, + {"type": "style", "section": "Styles"}, + {"type": "refactor", "section": "Code Refactoring"}, + {"type": "perf", "section": "Performance Improvements"}, + {"type": "test", "section": "Tests"}, + {"type": "build", "section": "Build System"}, + {"type": "ci", "section": "CI/CD"}, + {"type": "chore", "section": "Chores"} + ], + "commitUrlFormat": "https://github.com/Flamki/stellarmind/commit/{{hash}}", + "compareUrlFormat": "https://github.com/Flamki/stellarmind/compare/{{previousTag}}...{{currentTag}}", + "issueUrlFormat": "https://github.com/Flamki/stellarmind/issues/{{id}}" +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d8fecba --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog + +All notable changes to StellarMind will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] — 2026-08-09 + +### Added +- Initial release +- Agent orchestration platform +- Multi-provider LLM support +- REST API with OpenAPI specification +- Web dashboard with real-time SSE updates +- Docker support for reproducible deployment +- Circuit breaker for upstream provider stability +- Audit history persistence for orchestration events +- Comprehensive documentation and contributor guides + +_Generated for Stellar Wave bounty #33_ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f635f68 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +# StellarMind Docker Image +# Multi-stage build for optimal image size + +FROM node:20-alpine AS builder + +WORKDIR /app + +# Install dependencies +COPY package*.json ./ +RUN npm ci --only=production + +# Copy source +COPY src/ ./src/ +COPY public/ ./public/ + +FROM node:20-alpine AS runtime + +WORKDIR /app + +# Create non-root user +RUN addgroup -g 1001 stellarmind && \ + adduser -u 1001 -G stellarmind -s /bin/sh -D stellarmind + +# Copy from builder +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/src ./src +COPY --from=builder /app/public ./public +COPY package*.json ./ +COPY .env.example ./ + +# Expose port +EXPOSE 3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 + +USER stellarmind + +CMD ["node", "src/server.js"] + +# Generated for Stellar Wave bounty #28 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c84157b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,46 @@ +version: '3.8' + +services: + stellarmind: + build: . + container_name: stellarmind + ports: + - "3000:3000" + env_file: + - .env + environment: + - NODE_ENV=production + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--spider", "http://localhost:3000/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + volumes: + - ./logs:/app/logs + networks: + - stellarmind + + # Optional: Redis for session/rate-limiting + redis: + image: redis:7-alpine + container_name: stellarmind-redis + restart: unless-stopped + ports: + - "6379:6379" + volumes: + - redis_data:/data + networks: + - stellarmind + profiles: + - full + +networks: + stellarmind: + driver: bridge + +volumes: + redis_data: + +# Generated for Stellar Wave bounty #28 diff --git a/docs/issue-task-checklist.md b/docs/issue-task-checklist.md new file mode 100644 index 0000000..2a54b83 --- /dev/null +++ b/docs/issue-task-checklist.md @@ -0,0 +1,54 @@ +# Issue Task Checklist Template + +Use this template when creating or working on issues. + +## Template + +```markdown +### 📋 Task Checklist + +#### Preparation +- [ ] Read CONTRIBUTING.md +- [ ] Check repository map (`docs/repository-map.md`) for relevant files +- [ ] Set up local environment (see `.env.example`) +- [ ] Create feature branch: `git checkout -b feat/issue-NNN` + +#### Implementation +- [ ] Core logic implemented +- [ ] Edge cases handled +- [ ] Error handling added +- [ ] Logging added where appropriate +- [ ] No hardcoded secrets or credentials + +#### Testing +- [ ] Unit tests written and passing +- [ ] Integration tests passing +- [ ] Manual smoke test performed +- [ ] Edge case tests included + +#### Code Quality +- [ ] Linting passes (`npm run lint`) +- [ ] No console.log left in production code +- [ ] Code follows existing patterns and conventions +- [ ] Comments explain "why", not "what" + +#### Documentation +- [ ] API changes documented in `docs/API_EXAMPLES.md` +- [ ] New environment variables added to `.env.example` +- [ ] Architecture changes reflected in `docs/architecture.md` +- [ ] README updated if needed + +#### Pre-PR +- [ ] Branch rebased on latest master +- [ ] All tests pass locally +- [ ] Self-review of diff completed +- [ ] PR description references related issues +- [ ] DCO signoff included + +#### Post-PR +- [ ] CI checks passing +- [ ] Review comments addressed +- [ ] Branch deleted after merge +``` + +_Generated for Stellar Wave bounty #47_ diff --git a/docs/openapi.yaml b/docs/openapi.yaml new file mode 100644 index 0000000..2cfd82c --- /dev/null +++ b/docs/openapi.yaml @@ -0,0 +1,199 @@ +openapi: "3.0.3" +info: + title: StellarMind API + description: REST API for the StellarMind agent orchestration platform + version: "1.0.0" + contact: + name: StellarMind Team +servers: + - url: http://localhost:3000/api/v1 + description: Local development +paths: + /agents: + get: + summary: List all registered agents + operationId: listAgents + tags: [Agents] + responses: + '200': + description: Agent list + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Agent' + post: + summary: Register a new agent + operationId: registerAgent + tags: [Agents] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AgentInput' + responses: + '201': + description: Agent registered + '400': + description: Invalid input + + /agents/{agentId}: + get: + summary: Get agent details + operationId: getAgent + tags: [Agents] + parameters: + - name: agentId + in: path + required: true + schema: + type: string + responses: + '200': + description: Agent details + content: + application/json: + schema: + $ref: '#/components/schemas/Agent' + '404': + description: Agent not found + + /orchestration: + post: + summary: Start an orchestration run + operationId: startOrchestration + tags: [Orchestration] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OrchestrationInput' + responses: + '202': + description: Orchestration started + + /orchestration/{runId}/status: + get: + summary: Get orchestration run status + operationId: getOrchestrationStatus + tags: [Orchestration] + parameters: + - name: runId + in: path + required: true + schema: + type: string + responses: + '200': + description: Run status + content: + application/json: + schema: + $ref: '#/components/schemas/OrchestrationStatus' + + /budget: + get: + summary: Get budget information + operationId: getBudget + tags: [Budget] + responses: + '200': + description: Budget details + content: + application/json: + schema: + $ref: '#/components/schemas/Budget' + + /wallet: + get: + summary: Get wallet status + operationId: getWallet + tags: [Wallet] + responses: + '200': + description: Wallet details + + /health: + get: + summary: Health check + operationId: healthCheck + tags: [System] + responses: + '200': + description: System healthy + +components: + schemas: + Agent: + type: object + properties: + id: + type: string + name: + type: string + provider: + type: string + model: + type: string + status: + type: string + enum: [active, inactive, error] + createdAt: + type: string + format: date-time + + AgentInput: + type: object + required: [name, provider, model] + properties: + name: + type: string + provider: + type: string + model: + type: string + config: + type: object + + OrchestrationInput: + type: object + required: [task] + properties: + task: + type: string + agents: + type: array + items: + type: string + budget: + type: number + + OrchestrationStatus: + type: object + properties: + runId: + type: string + status: + type: string + enum: [pending, running, completed, failed] + progress: + type: number + result: + type: object + + Budget: + type: object + properties: + total: + type: number + spent: + type: number + remaining: + type: number + currency: + type: string + +# Generated for Stellar Wave bounty #36 diff --git a/docs/repository-map.md b/docs/repository-map.md new file mode 100644 index 0000000..7611925 --- /dev/null +++ b/docs/repository-map.md @@ -0,0 +1,48 @@ +# StellarMind Repository Map + +## For New Contributors + +Welcome! Here's where to find everything: + +### Core Source Code (`src/`) +| Directory/File | Purpose | +|---------------|---------| +| `src/agents/` | Agent orchestration, registry, budgeting, settlement | +| `src/middleware/` | Auth, error handling, rate limiting | +| `src/config.js` | Application configuration | +| `src/logger.js` | Structured logging | +| `src/pricing.config.js` | Pricing model configuration | + +### Frontend (`public/`) +| Directory/File | Purpose | +|---------------|---------| +| `public/assets/css/` | Stylesheets (layout, pages, sidebar, components) | +| `public/assets/js/` | Client-side scripts (SSE, wallet, agents, orchestration) | +| `public/index.html` | Main entry point | + +### Documentation (`docs/`) +| File | Purpose | +|------|---------| +| `docs/architecture.md` | System architecture overview | +| `docs/API_EXAMPLES.md` | REST API usage examples | + +### Configuration +| File | Purpose | +|------|---------| +| `.env.example` | Environment variables template | +| `package.json` | Dependencies and scripts | +| `eslint.config.js` | Linting rules | + +### Quick Start +1. `cp .env.example .env` — configure environment +2. `npm install` — install dependencies +3. `npm run dev` — start development server +4. Visit `http://localhost:3000` + +## Common Tasks +- **Adding a new agent**: See `src/agents/registry.js` +- **Adding a new API endpoint**: See `src/agents/services.js` +- **Modifying the UI**: See `public/assets/js/` and `public/assets/css/` +- **Testing**: `npm test` + +_Last updated: 2026-08-09 — Generated for Stellar Wave bounty #48_ diff --git a/src/agents/circuit-breaker.js b/src/agents/circuit-breaker.js new file mode 100644 index 0000000..d16c726 --- /dev/null +++ b/src/agents/circuit-breaker.js @@ -0,0 +1,174 @@ +/** + * Circuit Breaker Module — StellarMind + * Protects against unstable upstream model providers. + * Stellar Wave bounty #23 + */ + +const STATES = { + CLOSED: 'CLOSED', // Normal operation — requests pass through + OPEN: 'OPEN', // Failure threshold exceeded — requests blocked + HALF_OPEN: 'HALF_OPEN', // Testing if provider has recovered +} + +class CircuitBreaker { + /** + * @param {Object} options + * @param {string} options.name — circuit name (e.g., provider name) + * @param {number} [options.failureThreshold=5] — failures before opening + * @param {number} [options.resetTimeout=30000] — ms before attempting half-open + * @param {number} [options.successThreshold=2] — successes needed to close + * @param {number} [options.requestTimeout=15000] — ms before request considered failed + */ + constructor(_options = {}) { + this.name = options.name || 'default' + this.failureThreshold = options.failureThreshold || 5 + this.resetTimeout = options.resetTimeout || 30000 + this.successThreshold = options.successThreshold || 2 + this.requestTimeout = options.requestTimeout || 15000 + + this.state = STATES.CLOSED + this.failureCount = 0 + this.successCount = 0 + this.lastFailureTime = null + this.lastFailureError = null + this.totalFailures = 0 + this.totalSuccesses = 0 + } + + /** + * Execute an async function with circuit breaker protection. + * @param {Function} fn — async function to execute + * @returns {Promise<*>} result of fn if circuit is closed/half-open and fn succeeds + * @throws {Error} if circuit is open or fn fails while half-open + */ + async execute(fn) { + if (this.state === STATES.OPEN) { + if (Date.now() - this.lastFailureTime >= this.resetTimeout) { + this.state = STATES.HALF_OPEN + this.successCount = 0 + } else { + throw new CircuitOpenError(this.name, this.lastFailureTime, this.resetTimeout) + } + } + + try { + const result = await this._withTimeout(fn()) + this._onSuccess() + return result + } catch (error) { + this._onFailure(error) + throw error + } + } + + _onSuccess() { + this.totalSuccesses++ + if (this.state === STATES.HALF_OPEN) { + this.successCount++ + if (this.successCount >= this.successThreshold) { + this.state = STATES.CLOSED + this.failureCount = 0 + } + } else { + // In CLOSED state, occasional successes reset the failure window + this.failureCount = Math.max(0, this.failureCount - 1) + } + } + + _onFailure(error) { + this.totalFailures++ + this.failureCount++ + this.lastFailureTime = Date.now() + this.lastFailureError = error.message || String(error) + + if (this.state === STATES.HALF_OPEN) { + this.state = STATES.OPEN + } else if (this.failureCount >= this.failureThreshold) { + this.state = STATES.OPEN + } + } + + async _withTimeout(promise) { + let timer + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error('Request timeout')), this.requestTimeout) + }) + try { + return await Promise.race([promise, timeout]) + } finally { + clearTimeout(timer) + } + } + + /** + * Get current circuit breaker status. + */ + getStatus() { + return { + name: this.name, + state: this.state, + failureCount: this.failureCount, + successCount: this.successCount, + totalFailures: this.totalFailures, + totalSuccesses: this.totalSuccesses, + lastFailureTime: this.lastFailureTime, + lastFailureError: this.lastFailureError, + failureThreshold: this.failureThreshold, + resetTimeout: this.resetTimeout, + } + } + + /** + * Force the circuit breaker open (e.g., manual intervention). + */ + forceOpen() { + this.state = STATES.OPEN + this.lastFailureTime = Date.now() + } + + /** + * Force the circuit breaker closed (reset). + */ + forceClose() { + this.state = STATES.CLOSED + this.failureCount = 0 + this.successCount = 0 + } +} + +class CircuitOpenError extends Error { + constructor(name, lastFailureTime, resetTimeout) { + const remaining = Math.max(0, resetTimeout - (Date.now() - lastFailureTime)) + super(`Circuit "${name}" is OPEN. Resets in ${Math.round(remaining / 1000)}s.`) + this.name = 'CircuitOpenError' + this.circuitName = name + this.resetsIn = remaining + } +} + +class CircuitBreakerRegistry { + constructor() { + this.breakers = new Map() + } + + get(name, options) { + if (!this.breakers.has(name)) { + this.breakers.set(name, new CircuitBreaker({ name, ...options })) + } + return this.breakers.get(name) + } + + getAllStatus() { + return [...this.breakers.values()].map((b) => b.getStatus()) + } +} + +const registry = new CircuitBreakerRegistry() + +module.exports = { + CircuitBreaker, + CircuitOpenError, + CircuitBreakerRegistry, + registry, + STATES, +} diff --git a/src/audit/index.js b/src/audit/index.js new file mode 100644 index 0000000..b822346 --- /dev/null +++ b/src/audit/index.js @@ -0,0 +1,136 @@ +/** + * Audit History Module — StellarMind + * Persists orchestration and payment events for audit history. + * Stellar Wave bounty #26 + */ + +const fs = require('fs').promises +const path = require('path') +const crypto = require('crypto') + +const AUDIT_DIR = process.env.AUDIT_DIR || path.join(__dirname, '..', '..', 'logs', 'audit') +const RETENTION_DAYS = parseInt(process.env.AUDIT_RETENTION_DAYS || '90', 10) + +class AuditLogger { + constructor() { + this.initialized = false + } + + async init() { + await fs.mkdir(AUDIT_DIR, { recursive: true }) + this.initialized = true + } + + /** + * Generate a deterministic event ID from event data. + */ + _generateEventId(event) { + const payload = JSON.stringify({ + type: event.type, + entityId: event.entityId, + timestamp: event.timestamp, + action: event.action, + }) + return crypto.createHash('sha256').update(payload).digest('hex').slice(0, 16) + } + + /** + * Get the log file path for a given date. + */ + _getLogPath(date) { + const d = date || new Date() + const yyyy = d.getFullYear() + const mm = String(d.getMonth() + 1).padStart(2, '0') + const dd = String(d.getDate()).padStart(2, '0') + return path.join(AUDIT_DIR, `audit-${yyyy}-${mm}-${dd}.jsonl`) + } + + /** + * Record an orchestration event. + * @param {Object} event + * @param {'orchestration'|'payment'|'agent'|'system'} event.type + * @param {string} event.entityId — run ID, payment ID, agent ID, etc. + * @param {string} event.action — 'started', 'completed', 'failed', etc. + * @param {Object} [event.metadata] — additional data + */ + async record(event) { + if (!this.initialized) await this.init() + + const entry = { + eventId: this._generateEventId(event), + type: event.type, + entityId: event.entityId, + action: event.action, + timestamp: event.timestamp || new Date().toISOString(), + metadata: event.metadata || {}, + } + + const logPath = this._getLogPath(new Date(entry.timestamp)) + const line = JSON.stringify(entry) + '\n' + await fs.appendFile(logPath, line, 'utf8') + return entry.eventId + } + + /** + * Query audit events within a date range. + * @param {Object} filters + * @param {string} [filters.type] — event type filter + * @param {string} [filters.entityId] — entity ID filter + * @param {Date} [filters.from] — start date + * @param {Date} [filters.to] — end date + * @param {number} [filters.limit] — max results (default 100) + */ + async query(filters = {}) { + const { type, entityId, from, to, limit = 100 } = filters + const results = [] + const start = from || new Date(Date.now() - RETENTION_DAYS * 86400000) + const end = to || new Date() + + const current = new Date(start) + while (current <= end) { + const logPath = this._getLogPath(current) + try { + const content = await fs.readFile(logPath, 'utf8') + for (const line of content.trim().split('\n')) { + if (!line) continue + try { + const entry = JSON.parse(line) + if (type && entry.type !== type) continue + if (entityId && entry.entityId !== entityId) continue + results.push(entry) + if (results.length >= limit) return results + } catch (_) { + /* skip malformed lines */ + } + } + } catch (e) { + if (e.code !== 'ENOENT') throw e + } + current.setDate(current.getDate() + 1) + } + + return results + } + + /** + * Clean up audit logs older than retention period. + */ + async cleanup() { + const cutoff = new Date(Date.now() - RETENTION_DAYS * 86400000) + const files = await fs.readdir(AUDIT_DIR) + for (const file of files) { + if (!file.startsWith('audit-') || !file.endsWith('.jsonl')) continue + const dateStr = file.replace('audit-', '').replace('.jsonl', '') + const [yyyy, mm, dd] = dateStr.split('-').map(Number) + const fileDate = new Date(yyyy, mm - 1, dd) + if (fileDate < cutoff) { + await fs.unlink(path.join(AUDIT_DIR, file)) + } + } + } +} + +// Singleton +const auditLogger = new AuditLogger() + +module.exports = { AuditLogger, auditLogger } diff --git a/src/prompts/agent-system.txt b/src/prompts/agent-system.txt new file mode 100644 index 0000000..e693c37 --- /dev/null +++ b/src/prompts/agent-system.txt @@ -0,0 +1,15 @@ +You are {agent_name}, an AI agent running on the StellarMind orchestration platform. + +Your task: {task_description} + +Context: +{context} + +Guidelines: +- Be concise and accurate +- If unsure, acknowledge uncertainty rather than fabricate information +- Follow the output format specified in the task +- Respect budget constraints: you have {budget_remaining} credits remaining +- Report completion with a summary of actions taken + +Current time: {current_time} diff --git a/src/prompts/index.js b/src/prompts/index.js new file mode 100644 index 0000000..dba5ee6 --- /dev/null +++ b/src/prompts/index.js @@ -0,0 +1,62 @@ +/** + * Prompt Template Loader — StellarMind + * Loads and renders versioned prompt templates. + * Stellar Wave bounty #24 + */ + +const fs = require('fs') +const path = require('path') + +const PROMPTS_DIR = path.join(__dirname) + +const CACHE = new Map() + +/** + * Load a prompt template from disk. + * Templates support {variable} placeholders. + * @param {string} name — template filename without .txt extension + * @returns {string} raw template + */ +function loadTemplate(name) { + if (CACHE.has(name)) return CACHE.get(name) + const filePath = path.join(PROMPTS_DIR, `${name}.txt`) + if (!fs.existsSync(filePath)) { + throw new Error(`Prompt template not found: ${name}.txt`) + } + const content = fs.readFileSync(filePath, 'utf8') + CACHE.set(name, content) + return content +} + +/** + * Render a prompt template with variables. + * @param {string} name — template name + * @param {Object} variables — key-value pairs to substitute + * @returns {string} rendered prompt + */ +function render(name, variables = {}) { + let template = loadTemplate(name) + for (const [key, value] of Object.entries(variables)) { + template = template.replace(new RegExp(`\\{${key}\\}`, 'g'), String(value)) + } + return template +} + +/** + * List all available prompt templates. + */ +function listTemplates() { + return fs + .readdirSync(PROMPTS_DIR) + .filter((f) => f.endsWith('.txt')) + .map((f) => f.replace('.txt', '')) +} + +/** + * Reload all templates (clears cache). + */ +function reloadAll() { + CACHE.clear() +} + +module.exports = { loadTemplate, render, listTemplates, reloadAll } diff --git a/src/prompts/orchestrator-routing.txt b/src/prompts/orchestrator-routing.txt new file mode 100644 index 0000000..8493ac0 --- /dev/null +++ b/src/prompts/orchestrator-routing.txt @@ -0,0 +1,16 @@ +You are the StellarMind orchestrator. Your role is to route tasks to the most appropriate agent. + +Available agents: +{agent_list} + +Incoming task: +{task} + +Instructions: +1. Analyze the task requirements +2. Select the best agent based on capability match +3. If no agent is suitable, respond with 'NO_SUITABLE_AGENT' +4. If multiple agents could handle it, choose the most cost-effective option +5. Output format: JSON with 'selectedAgent', 'confidence' (0-1), and 'reasoning' + +Respond only with valid JSON. diff --git a/src/prompts/settlement-review.txt b/src/prompts/settlement-review.txt new file mode 100644 index 0000000..efeb4d6 --- /dev/null +++ b/src/prompts/settlement-review.txt @@ -0,0 +1,14 @@ +You are the StellarMind settlement reviewer. Your role is to evaluate agent work and authorize payment. + +Agent: {agent_name} +Task: {task_description} +Result: {result} +Budget used: {budget_used} / {budget_total} + +Evaluation criteria: +1. Was the task completed successfully? (yes/no) +2. Quality score (1-10): accuracy, completeness, adherence to format +3. Should payment be released? (yes/no with reason) +4. Any issues or regressions identified? + +Output format: JSON with 'completed', 'qualityScore', 'releasePayment', 'reason', 'issues' diff --git a/src/providers/index.js b/src/providers/index.js new file mode 100644 index 0000000..92a807e --- /dev/null +++ b/src/providers/index.js @@ -0,0 +1,95 @@ +/** + * Provider Abstraction Module — StellarMind + * Multi-LLM provider support with unified interface. + * Stellar Wave bounty #25 + */ + +const PROVIDER_REGISTRY = new Map() + +class ProviderInterface { + /** + * @param {Object} config + * @param {string} config.apiKey + * @param {string} [config.baseUrl] + * @param {Object} [config.defaultOptions] + */ + constructor(config) { + this.config = config + this.name = 'base' + } + + /** Returns the list of supported models */ + async listModels() { + throw new Error('Not implemented') + } + + /** Execute a completion request */ + async complete(_params) { + throw new Error('Not implemented') + } + + /** Execute a chat completion request */ + async chat(messages, options = {}) { + throw new Error('Not implemented') + } + + /** Health check against the provider */ + async healthCheck() { + throw new Error('Not implemented') + } + + /** Provider-specific token counting */ + async countTokens(text) { + // Default: rough estimate (4 chars ~= 1 token) + return Math.ceil(text.length / 4) + } +} + +/** + * Register a provider implementation. + * @param {string} name + * @param {typeof ProviderInterface} ProviderClass + */ +function registerProvider(name, ProviderClass) { + PROVIDER_REGISTRY.set(name.toLowerCase(), ProviderClass) +} + +/** + * Create a provider instance. + * @param {string} name — provider name (e.g., 'openai', 'anthropic', 'local') + * @param {Object} config — provider-specific configuration + * @returns {ProviderInterface} + */ +function createProvider(name, config) { + const ProviderClass = PROVIDER_REGISTRY.get(name.toLowerCase()) + if (!ProviderClass) { + throw new Error( + `Unknown provider: ${name}. Available: ${[...PROVIDER_REGISTRY.keys()].join(', ')}` + ) + } + return new ProviderClass(config) +} + +/** + * List all registered provider names. + */ +function listProviders() { + return [...PROVIDER_REGISTRY.keys()] +} + +/** + * Suggested usage pattern: + * + * const { createProvider } = require('./providers'); + * const provider = createProvider('openai', { apiKey: process.env.OPENAI_API_KEY }); + * const models = await provider.listModels(); + * const response = await provider.chat([{ role: 'user', content: 'Hello' }]); + */ + +module.exports = { + ProviderInterface, + registerProvider, + createProvider, + listProviders, + PROVIDER_REGISTRY, +}