diff --git a/src/bus/index.ts b/src/bus/index.ts index 9d79eac..c638713 100644 --- a/src/bus/index.ts +++ b/src/bus/index.ts @@ -21,6 +21,7 @@ import { GenesisEventBus } from './event-bus.js'; // Re-export types and classes export { GenesisEventBus } from './event-bus.js'; export type { Subscription, SubscribeOptions, BusStats } from './event-bus.js'; +export { createTypedPublisher, createTypedSubscriber, type TypedPublisher, type TypedSubscriber } from './typed-publisher.js'; export type { // Base diff --git a/src/core/bootstrap.ts b/src/core/bootstrap.ts index 2f8a360..f0f8718 100644 --- a/src/core/bootstrap.ts +++ b/src/core/bootstrap.ts @@ -32,49 +32,30 @@ import { getDIContainer, type DIContainer } from '../di/container.js'; * 3. Use resolve('yourToken') */ export interface ServiceTokenMap { - // Infrastructure + // Infrastructure (L1) eventBus: import('../bus/index.js').GenesisEventBus; - config: any; // TODO: type when config module is typed fek: import('../kernel/free-energy-kernel.js').FreeEnergyKernel; neuromodulation: import('../neuromodulation/index.js').NeuromodulationSystem; nociception: import('../nociception/index.js').NociceptiveSystem; allostasis: import('../allostasis/index.js').AllostasisSystem; daemon: import('../daemon/index.js').Daemon; - // Memory & Persistence + // Memory & Persistence (L2) memory: import('../memory/index.js').MemorySystem; - persistence: any; - graphRAG: any; - // Cognition + // Cognition (L3) brain: import('../brain/index.js').Brain; consciousness: import('../consciousness/index.js').ConsciousnessSystem; worldModel: import('../world-model/index.js').WorldModelSystem; thinking: import('../thinking/index.js').ThinkingEngine; metacognitive: import('../reasoning/metacognitive-controller.js').MetacognitiveController; grounding: import('../grounding/index.js').GroundingSystem; - mctsEngine: any; - outcomeIntegrator: any; - - // Autonomous - goalSystem: any; - attentionController: any; - selfReflection: any; governance: import('../governance/index.js').GovernanceSystem; - // Market & Content - marketStrategist: any; - contentOrchestrator: any; - newsletter: any; - mcpClient: any; - - // Tools & Agents + // Tools & Agents (L3) toolRegistry: Map; agentPool: import('../agents/index.js').AgentPool; - // Observability - dashboard: any; - // Core (v35) agentLoop: import('./agent-loop.js').AgentLoop; } diff --git a/src/core/effect.ts b/src/core/effect.ts index c7c6ca1..6da05b6 100644 --- a/src/core/effect.ts +++ b/src/core/effect.ts @@ -67,6 +67,8 @@ export function isLeft(e: Either): e is Left { export type Effect = { readonly _tag: 'Effect'; readonly run: (env: R) => Promise>; + /** Optional synchronous evaluator — set by `sync()` and `succeed()` for `runSync` support. */ + readonly _sync?: (env: R) => Either; }; // ============================================================================ @@ -81,9 +83,11 @@ export type Effect = { * // Effect */ export function succeed(value: A): Effect { + const r = right(value); return { _tag: 'Effect', - run: (_env) => Promise.resolve(right(value)), + run: (_env) => Promise.resolve(r), + _sync: (_env) => r, }; } @@ -142,6 +146,7 @@ export function sync(fn: () => A): Effect { return { _tag: 'Effect', run: (_env) => Promise.resolve(right(fn())), + _sync: (_env) => right(fn()), }; } @@ -321,6 +326,7 @@ export function provide( return { _tag: 'Effect', run: (_ignoredEnv: unknown) => effect.run(env), + _sync: effect._sync ? (_ignoredEnv: unknown) => effect._sync!(env) : undefined, }; } @@ -449,39 +455,20 @@ export async function runEither(effect: Effect): Promise computePhi(state))); */ export function runSync(effect: Effect): A { - // We rely on the invariant that sync/succeed return already-settled Promises. - // There is no way to block a Promise synchronously in Node without native - // extensions, so we surface the result through a shared slot. - let resolved = false; - let value: A | undefined; - let threw: unknown; - - effect.run(undefined).then( - (result) => { - resolved = true; - if (isRight(result)) { - value = result.value; - } else { - // Effect guarantees this branch is unreachable at compile - // time, but we defend against incorrect runtime usage. - threw = result.error; - } - }, - (err) => { - resolved = true; - threw = err; - }, - ); - - if (!resolved) { - throw new Error( - 'runSync called on an asynchronous Effect. ' + - 'Use runPromise or runEither for Effects that perform I/O.', - ); + // Use the synchronous evaluator if available (set by sync/succeed). + if (effect._sync) { + const result = effect._sync(undefined); + if (isRight(result)) return result.value; + // Effect guarantees this branch is unreachable at compile time, + // but we defend against incorrect runtime usage. + throw (result as Left).error; } - if (threw !== undefined) throw threw; - return value as A; + throw new Error( + 'runSync called on an Effect without a synchronous evaluator. ' + + 'Only Effects created via sync() or succeed() support runSync. ' + + 'Use runPromise or runEither for Effects that perform I/O.', + ); } // ============================================================================ diff --git a/src/core/fek-brain-bridge.ts b/src/core/fek-brain-bridge.ts index 8f433ef..859b6c8 100644 --- a/src/core/fek-brain-bridge.ts +++ b/src/core/fek-brain-bridge.ts @@ -19,6 +19,8 @@ * bridge.feedbackResult(result); */ +import { estimateComplexity } from '../thinking/budget-forcing.js'; + // ============================================================================ // Types // ============================================================================ @@ -122,45 +124,11 @@ function selectStrategy( } /** - * Estimate query complexity using fast heuristics (no LLM call). - * - * Factors: - * - Length (longer = more complex) - * - Question words (what/why/how) - * - Domain keywords - * - Nested structure (clauses, conditions) - * - Multi-step indicators + * Estimate query complexity using the budget-forcing module's algorithm. + * Delegates to estimateComplexity() to avoid duplication. */ function estimateQueryComplexity(query: string): number { - const lower = query.toLowerCase(); - let complexity = 0; - - // Length factor (0-0.3) - const wordCount = query.split(/\s+/).length; - complexity += Math.min(0.3, wordCount / 200); - - // Deep reasoning indicators (0-0.3) - const deepWords = ['why', 'how', 'explain', 'analyze', 'compare', 'evaluate', - 'design', 'architect', 'optimize', 'debug', 'prove', 'derive']; - const deepCount = deepWords.filter(w => lower.includes(w)).length; - complexity += Math.min(0.3, deepCount * 0.1); - - // Multi-step indicators (0-0.2) - const multiStep = ['and then', 'first', 'second', 'step', 'after that', - 'followed by', 'next', 'finally', 'also', 'additionally']; - const stepCount = multiStep.filter(w => lower.includes(w)).length; - complexity += Math.min(0.2, stepCount * 0.05); - - // Nested structure (0-0.1) - const nestingIndicators = (query.match(/[({[\]})]/g) || []).length; - complexity += Math.min(0.1, nestingIndicators * 0.02); - - // Code indicators (0-0.1) - if (/```|function\s|class\s|import\s|const\s/.test(query)) { - complexity += 0.1; - } - - return Math.min(1, complexity); + return estimateComplexity(query).score; } /** @@ -235,7 +203,8 @@ export class FEKBrainBridge { } = {} ): FEKRouting { // 1. Run FEK cycle to get current free energy - let freeEnergy = 0.5; + // Default 0.1 = calm/low-uncertainty when no FEK; query complexity drives strategy + let freeEnergy = 0.1; let fekStrategy = 'sequential'; if (this.fek) { diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 0000000..f237dbb --- /dev/null +++ b/src/core/index.ts @@ -0,0 +1,50 @@ +/** + * Genesis v35 — Core Module Barrel + * + * Re-exports all core architectural modules for clean imports: + * import { createLogger, AgentLoop, FEKBrainBridge } from '../core/index.js'; + */ + +// Structured logging +export { createLogger, getLogger, setLogger, logger, type Logger, type LogLevel } from './logger.js'; + +// Typed effect system +export { + // Either + right, left, isRight, isLeft, + type Right, type Left, type Either, + // Effect constructors + succeed, fail, tryPromise, sync, tryCatch, fromNullable, + type Effect, + // Combinators + map, flatMap, catchAll, tap, tapError, provide, + // Concurrency + all, allSettled, race, + // Runtime + runPromise, runEither, runSync, + // Utilities + timeout, wrapLegacy, pipe, + // Error types + GenesisError, LLMError, ToolError, MemoryError, + BusError, ConfigError, TimeoutError, +} from './effect.js'; + +// FEK ↔ Brain bridge +export { + FEKBrainBridge, + type FEKSnapshot, type FEKRouting, type ThinkingStrategy, + type BrainModuleHint, type TokenBudget, type BrainStepFeedback, +} from './fek-brain-bridge.js'; + +// Agent loop (CoALA) +export { AgentLoop, type AgentModules } from './agent-loop.js'; + +// DI typed resolution +export { + bootstrap, resolve, resolveSync, + hasService, isResolved, shutdown, getContainer, + type ServiceTokenMap, type ServiceToken, +} from './bootstrap.js'; + +// Module registry (L1→L4 boot sequencing) +export { ModuleRegistry, registerGenesisModules, getModuleRegistry } from './module-registry.js'; diff --git a/src/core/logger.ts b/src/core/logger.ts index 870d89d..c8d176e 100644 --- a/src/core/logger.ts +++ b/src/core/logger.ts @@ -145,9 +145,9 @@ function wrapPino(pinoLogger: PinoLogger): Logger { ): (o: Record | string, m?: string | unknown, ...r: unknown[]) => void { return (o, m, ..._rest) => { if (typeof o === 'string') { - fn.call(pinoLogger, o); + (fn as Function).call(pinoLogger, o, m, ..._rest); } else { - fn.call(pinoLogger, o, typeof m === 'string' ? m : ''); + (fn as Function).call(pinoLogger, o, typeof m === 'string' ? m : ''); } }; } diff --git a/src/core/module-registry.ts b/src/core/module-registry.ts index 48f9187..9d90fa1 100644 --- a/src/core/module-registry.ts +++ b/src/core/module-registry.ts @@ -17,6 +17,7 @@ * genesis.ts can delegate its boot fields to this registry incrementally. */ +import { createLogger } from './logger.js'; import { getFreeEnergyKernel } from '../kernel/free-energy-kernel.js'; import { getBrain } from '../brain/index.js'; import { getEventBus } from '../bus/index.js'; @@ -35,6 +36,12 @@ import { getAgentPool } from '../agents/index.js'; import { getGovernanceSystem } from '../governance/index.js'; import { getSelfImprovementEngine } from '../self-modification/index.js'; +// ============================================================================ +// Logging +// ============================================================================ + +const log = createLogger('module-registry'); + // ============================================================================ // Public types // ============================================================================ @@ -278,9 +285,8 @@ export class ModuleRegistry { entry.health.bootTimeMs = Date.now() - t0; if (entry.opts.optional) { - console.warn( - `[ModuleRegistry] Optional module '${entry.id}' failed to boot — ` + - `continuing. ${msg}`, + log.warn( + `Optional module '${entry.id}' failed to boot — continuing. ${msg}`, ); } else { throw new Error( @@ -368,8 +374,8 @@ export class ModuleRegistry { entry.bootedAt = null; } catch (err) { const msg = err instanceof Error ? err.message : String(err); - console.warn( - `[ModuleRegistry] Shutdown error for '${entry.id}': ${msg}`, + log.warn( + `Shutdown error for '${entry.id}': ${msg}`, ); entry.health.status = 'degraded'; } diff --git a/src/market-strategist/gamma-renderer.ts b/src/market-strategist/gamma-renderer.ts new file mode 100644 index 0000000..b23ccb0 --- /dev/null +++ b/src/market-strategist/gamma-renderer.ts @@ -0,0 +1,272 @@ +/** + * Genesis — Gamma.app API Renderer + * + * Converts a PresentationSpec into Gamma.app input, generates a + * professional presentation via the Gamma API, and exports PPTX + PDF. + * + * Requires: + * GAMMA_API_KEY — Gamma API key + * GAMMA_THEME_ID — Pre-created Rossignoli & Partners theme ID (optional) + */ + +import type { PresentationSpec, SlideSpec } from '../presentation/types.js'; + +// ============================================================================ +// Types +// ============================================================================ + +export interface GammaRenderResult { + success: boolean; + generationId?: string; + gammaUrl?: string; + pptxUrl?: string; + pdfUrl?: string; + error?: string; +} + +interface GammaGenerationResponse { + id: string; + status: 'pending' | 'in_progress' | 'completed' | 'failed'; + url?: string; + error?: string; +} + +// ============================================================================ +// Gamma API Client +// ============================================================================ + +const GAMMA_API_BASE = 'https://public-api.gamma.app/v1.0'; + +/** + * Render a PresentationSpec via the Gamma.app API. + * + * 1. Convert spec → Gamma markdown input + * 2. POST to /generations + * 3. Poll until completed + * 4. Return URLs (Gamma viewer, PPTX export, PDF export) + */ +export async function renderViaGamma(spec: PresentationSpec): Promise { + const apiKey = process.env.GAMMA_API_KEY; + if (!apiKey) { + return { success: false, error: 'GAMMA_API_KEY not set' }; + } + + try { + // 1. Convert to Gamma input + const inputText = specToGammaMarkdown(spec); + + // 2. Create generation + const response = await fetch(`${GAMMA_API_BASE}/generations`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-KEY': apiKey, + }, + body: JSON.stringify({ + inputText, + textMode: 'preserve', + format: 'presentation', + ...(process.env.GAMMA_THEME_ID ? { themeId: process.env.GAMMA_THEME_ID } : {}), + numCards: Math.min(spec.slides.length, 60), + cardSplit: 'inputTextBreaks', + additionalInstructions: 'Institutional financial report style. Professional, minimal, data-driven. Executive summary first, then asset class sections with charts and commentary.', + textOptions: { + amount: 'detailed', + tone: 'professional', + audience: 'institutional investors', + }, + imageOptions: { + source: 'aiGenerated', + model: 'flux-1-pro', + style: 'professional', + }, + }), + signal: AbortSignal.timeout(30000), + }); + + if (!response.ok) { + const errText = await response.text(); + return { success: false, error: `Gamma API HTTP ${response.status}: ${errText}` }; + } + + const generation: GammaGenerationResponse = await response.json(); + console.log(` [gamma] Generation created: ${generation.id}`); + + // 3. Poll for completion (max 120s) + const result = await pollGeneration(apiKey, generation.id, 120000); + if (!result.success) return result; + + return result; + } catch (e) { + return { success: false, error: `Gamma render failed: ${(e as Error).message}` }; + } +} + +/** + * Poll a Gamma generation until completion or timeout. + */ +async function pollGeneration(apiKey: string, generationId: string, timeoutMs: number): Promise { + const start = Date.now(); + const pollInterval = 3000; // 3s between checks + + while (Date.now() - start < timeoutMs) { + await new Promise(r => setTimeout(r, pollInterval)); + + try { + const response = await fetch(`${GAMMA_API_BASE}/generations/${generationId}`, { + headers: { 'X-API-KEY': apiKey }, + signal: AbortSignal.timeout(10000), + }); + + if (!response.ok) { + console.warn(` [gamma] Poll HTTP ${response.status}`); + continue; + } + + const gen: GammaGenerationResponse = await response.json(); + + if (gen.status === 'completed') { + console.log(` [gamma] Generation completed: ${gen.url}`); + return { + success: true, + generationId, + gammaUrl: gen.url, + // Gamma provides export URLs via the generation response or a separate endpoint + pptxUrl: gen.url ? `${gen.url}/export/pptx` : undefined, + pdfUrl: gen.url ? `${gen.url}/export/pdf` : undefined, + }; + } + + if (gen.status === 'failed') { + return { success: false, generationId, error: gen.error || 'Generation failed' }; + } + + const elapsed = ((Date.now() - start) / 1000).toFixed(0); + console.log(` [gamma] Status: ${gen.status} (${elapsed}s elapsed)`); + } catch (e) { + console.warn(` [gamma] Poll error: ${(e as Error).message}`); + } + } + + return { success: false, generationId, error: `Gamma generation timed out after ${timeoutMs / 1000}s` }; +} + +// ============================================================================ +// Spec → Gamma Markdown Converter +// ============================================================================ + +/** + * Convert a PresentationSpec to Gamma-compatible markdown input. + * Uses \n---\n as card/slide breaks (Gamma's cardSplit: 'inputTextBreaks'). + */ +export function specToGammaMarkdown(spec: PresentationSpec): string { + const parts: string[] = []; + + for (const slide of spec.slides) { + parts.push(slideToMarkdown(slide)); + } + + return parts.join('\n---\n'); +} + +function slideToMarkdown(slide: SlideSpec): string { + const content = slide.content as any; + + switch (slide.type) { + case 'cover': + return [ + `# ${content.headline || content.title || ''}`, + content.subheadline ? `## ${content.subheadline}` : '', + content.date_range ? `*${content.date_range}*` : '', + content.company ? `**${content.company}**` : '', + ].filter(Boolean).join('\n\n'); + + case 'executive_summary': { + const lines = [`## ${content.title || 'Executive Summary'}`]; + if (content.sections) { + for (const section of content.sections) { + lines.push(`### ${section.label || ''}`); + lines.push(section.text || ''); + } + } + if (content.bullets) { + for (const b of content.bullets) lines.push(`- ${b}`); + } + return lines.join('\n\n'); + } + + case 'section_divider': + return `# ${content.title || ''}${content.subtitle ? `\n\n*${content.subtitle}*` : ''}`; + + case 'editorial': + return [ + `## ${content.title || ''}`, + content.commentary || '', + content.image_path ? `![Chart](file://${content.image_path})` : '', + content.source ? `*${content.source}*` : '', + ].filter(Boolean).join('\n\n'); + + case 'chart': + return [ + `## ${content.title || 'Chart'}`, + content.commentary || content.narrative || '', + content.source ? `*${content.source}*` : '', + ].filter(Boolean).join('\n\n'); + + case 'kpi_dashboard': { + const kpis = content.kpis || []; + if (kpis.length === 0) return `## ${content.title || 'KPIs'}`; + const header = '| Metric | Value | Change |'; + const sep = '|--------|-------|--------|'; + const rows = kpis.map((k: any) => `| ${k.label || ''} | ${k.value || ''} | ${k.delta || ''} |`); + return [`## ${content.title || 'Key Metrics'}`, header, sep, ...rows].join('\n'); + } + + case 'text': + return [ + content.title ? `## ${content.title}` : '', + content.body || content.text || '', + ].filter(Boolean).join('\n\n'); + + case 'callout': + return [ + content.title ? `## ${content.title}` : '', + content.text ? `> ${content.text}` : '', + ].filter(Boolean).join('\n\n'); + + case 'quote_slide': + return [ + content.quote ? `> "${content.quote}"` : '', + content.attribution ? `— *${content.attribution}*` : '', + ].filter(Boolean).join('\n\n'); + + case 'chart_grid': { + const lines = [`## ${content.title || 'Charts'}`]; + if (content.grid) { + for (const g of content.grid) { + lines.push(`- **${g.label || ''}**${g.image_path ? ` ![](file://${g.image_path})` : ''}`); + } + } + return lines.join('\n'); + } + + case 'sources': { + const lines = [`## Sources & Methodology`]; + if (content.sources) { + for (const s of content.sources) lines.push(`- ${s}`); + } + return lines.join('\n'); + } + + case 'back_cover': + return [ + `# ${content.company || 'Thank You'}`, + content.tagline || '', + content.contact ? (Array.isArray(content.contact) ? content.contact.join(' | ') : content.contact) : '', + content.website || '', + ].filter(Boolean).join('\n\n'); + + default: + return content.title ? `## ${content.title}` : `## ${slide.type}`; + } +} diff --git a/src/memory/activation.ts b/src/memory/activation.ts index 5e4ee47..a444874 100644 --- a/src/memory/activation.ts +++ b/src/memory/activation.ts @@ -255,7 +255,7 @@ export class ActivationEngine { const assocMap = this.associations.get(tag)!; const fan = assocMap.size; const strength = Math.max(0, this.config.maxAssociativeStrength - Math.log(Math.max(1, fan))); - for (const [mid] of assocMap) { + for (const mid of Array.from(assocMap.keys())) { assocMap.set(mid, strength); } } @@ -273,7 +273,7 @@ export class ActivationEngine { let bestMemory: ActivatedMemory | null = null; let bestActivation = -Infinity; - for (const memory of this.memories.values()) { + for (const memory of Array.from(this.memories.values())) { const activation = this.computeActivation(memory.id, context); if (activation > bestActivation) { bestActivation = activation; @@ -300,7 +300,7 @@ export class ActivationEngine { retrieveTopK(k: number, context: ContextElement[] = []): ActivatedMemory[] { const scored: Array<{ memory: ActivatedMemory; activation: number }> = []; - for (const memory of this.memories.values()) { + for (const memory of Array.from(this.memories.values())) { const activation = this.computeActivation(memory.id, context); if (activation > this.config.retrievalThreshold) { scored.push({ memory, activation }); @@ -321,7 +321,7 @@ export class ActivationEngine { ): ActivatedMemory[] { const scored: Array<{ memory: ActivatedMemory; activation: number }> = []; - for (const memory of this.memories.values()) { + for (const memory of Array.from(this.memories.values())) { if (!filter(memory)) continue; const activation = this.computeActivation(memory.id, context); if (activation > this.config.retrievalThreshold) { @@ -345,7 +345,7 @@ export class ActivationEngine { let pruned = 0; const toRemove: string[] = []; - for (const memory of this.memories.values()) { + for (const memory of Array.from(this.memories.values())) { const activation = this.computeActivation(memory.id, context); if (activation < this.config.retrievalThreshold - 2.0) { toRemove.push(memory.id); @@ -379,7 +379,7 @@ export class ActivationEngine { /** Get all memories (for debugging) */ getAll(): ActivatedMemory[] { - return [...this.memories.values()]; + return Array.from(this.memories.values()); } /** Get a specific memory by id */ @@ -393,7 +393,7 @@ export class ActivationEngine { let totalActivation = 0; let totalAge = 0; - for (const memory of this.memories.values()) { + for (const memory of Array.from(this.memories.values())) { totalActivation += memory.activation; totalAge += now - memory.createdAt; } diff --git a/src/memory/index.ts b/src/memory/index.ts index 5fe874c..3e92c64 100644 --- a/src/memory/index.ts +++ b/src/memory/index.ts @@ -146,6 +146,9 @@ export { DEFAULT_META_CONFIG, } from './meta-memory.js'; +// Re-export Activation Engine Module +export { ActivationEngine, type ActivatedMemory, type ContextElement, type ActivationConfig, type RetrievalResult as ACTRRetrievalResult } from './activation.js'; + import { EpisodicStore, createEpisodicStore, CreateEpisodicOptions } from './episodic.js'; import { SemanticStore, createSemanticStore, CreateSemanticOptions } from './semantic.js'; import { ProceduralStore, createProceduralStore, CreateProceduralOptions } from './procedural.js'; diff --git a/src/presentation/fetch_performance.py b/src/presentation/fetch_performance.py new file mode 100644 index 0000000..714ff2d --- /dev/null +++ b/src/presentation/fetch_performance.py @@ -0,0 +1,95 @@ +""" +Fetch asset performance data via yfinance (handles Yahoo rate limiting internally). +Called from TypeScript pipeline via child_process. +Outputs JSON to stdout. +""" +import json +import sys +from datetime import datetime, timedelta + +try: + import yfinance as yf +except ImportError: + print(json.dumps({"error": "yfinance not installed"})) + sys.exit(1) + +TICKERS = { + "S&P 500": "^GSPC", + "Nasdaq 100": "^NDX", + "Dow Jones": "^DJI", + "STOXX 600": "^STOXX", + "FTSE MIB": "FTSEMIB.MI", + "US 10Y": "^TNX", + "US 2Y": "^IRX", + "German 10Y": "DE10Y.F", + "EUR/USD": "EURUSD=X", + "USD/CHF": "CHF=X", + "Gold": "GC=F", + "Oil WTI": "CL=F", + "Bitcoin": "BTC-USD", + "VIX": "^VIX", +} + +def fmt_price(val, asset): + """Format price based on asset type.""" + if "10Y" in asset or "2Y" in asset: + return f"{val:.2f}%" + if "EUR" in asset or "CHF" in asset: + return f"{val:.4f}" + if val >= 1000: + return f"{val:,.2f}" + return f"{val:.2f}" + +def main(): + results = {} + now = datetime.now() + year_start = datetime(now.year, 1, 1) + + for asset, ticker in TICKERS.items(): + try: + t = yf.Ticker(ticker) + hist = t.history(period="6mo") + if hist.empty or len(hist) < 5: + continue + + closes = hist["Close"].dropna().values + dates = hist.index + + current = float(closes[-1]) + one_week_ago = float(closes[max(0, len(closes) - 6)]) + one_month_ago = float(closes[max(0, len(closes) - 22)]) + + # YTD base + ytd_base = float(closes[0]) + for i, d in enumerate(dates): + if d.tz_localize(None) >= year_start: + ytd_base = float(closes[i]) + break + + chg_1w = ((current - one_week_ago) / one_week_ago) * 100 + chg_mtd = ((current - one_month_ago) / one_month_ago) * 100 + chg_ytd = ((current - ytd_base) / ytd_base) * 100 + + fmt = lambda v: f"+{v:.1f}%" if v >= 0 else f"{v:.1f}%" + + if asset == "VIX": + signal = "bullish" if chg_1w < -5 else ("bearish" if chg_1w > 10 else "neutral") + else: + signal = "bullish" if chg_1w > 1 else ("bearish" if chg_1w < -1 else "neutral") + + results[asset] = { + "name": asset, + "level": fmt_price(current, asset), + "change1w": fmt(chg_1w), + "changeMtd": fmt(chg_mtd), + "changeYtd": fmt(chg_ytd), + "signal": signal, + "commentary": "", + } + except Exception as e: + sys.stderr.write(f"[yfinance] {asset}: {e}\n") + + print(json.dumps(results)) + +if __name__ == "__main__": + main() diff --git a/src/thinking/budget-forcing.ts b/src/thinking/budget-forcing.ts index 9356964..3273499 100644 --- a/src/thinking/budget-forcing.ts +++ b/src/thinking/budget-forcing.ts @@ -648,7 +648,7 @@ export function planBudget( } // ============================================================================ -// Required types: BudgetForcingConfig, SubQuestionBudget, BudgetPlan, +// Required types: BudgetForcingConfig, SubQuestionBudget, ForcingBudgetPlan, // BudgetForcingResult // ============================================================================ @@ -702,7 +702,7 @@ export interface SubQuestionBudget { /** * Full decomposition plan returned by `PlanAndBudgetAllocator`. */ -export interface BudgetPlan { +export interface ForcingBudgetPlan { /** Ordered list of sub-questions with individual token allocations. */ subQuestions: SubQuestionBudget[]; /** Sum of all `budgetTokens` — equals the input `totalBudget` (modulo rounding). */ @@ -886,7 +886,7 @@ export class PlanAndBudgetAllocator { } /** - * Produce a `BudgetPlan` for `query` given `totalBudget` tokens. + * Produce a `ForcingBudgetPlan` for `query` given `totalBudget` tokens. * * Steps: * 1. Decompose query into sub-questions. @@ -895,7 +895,7 @@ export class PlanAndBudgetAllocator { * 4. Multiply by `totalBudget`; assign integer tokens (remainder to last). * 5. Assign priorities: earlier sub-questions get lower (higher-priority) numbers. */ - allocate(query: string, totalBudget: number): BudgetPlan { + allocate(query: string, totalBudget: number): ForcingBudgetPlan { const subQuestions = this._decomposer.decompose(query); // Score each sub-question independently. diff --git a/src/tool-factory/index.ts b/src/tool-factory/index.ts index 8869f4e..15a6987 100644 --- a/src/tool-factory/index.ts +++ b/src/tool-factory/index.ts @@ -218,3 +218,4 @@ export { ToolVerifier } from './verifier.js'; export { ToolPromoter } from './promoter.js'; export { ToolLibrary } from './library.js'; export { DynamicToolRegistry } from './registry.js'; +export { MAPElitesArchive, createToolArchive, describeToolBehavior, toolFitness, type DimensionSpec, type MAPElitesConfig } from './map-elites.js'; diff --git a/src/tools/index.ts b/src/tools/index.ts index 6718a6b..d75432c 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -287,18 +287,17 @@ toolRegistry.set('git_push', { }, }); -// Register presentation tool -import { generatePresentation } from './presentation.js'; -import type { PresentationSpec } from '../presentation/types.js'; - +// Register presentation tool (lazy import — module may not exist in all builds) toolRegistry.set('presentation', { name: 'presentation', description: 'Generate institutional-quality PPTX presentation from JSON spec', execute: async (params: Record) => { - return generatePresentation(params.spec as PresentationSpec); + const { generatePresentation } = await import('./presentation.js'); + const spec = params.spec as import('../presentation/types.js').PresentationSpec; + return generatePresentation(spec); }, validate: (params: Record) => { - const spec = params.spec as PresentationSpec | undefined; + const spec = params.spec as { slides?: unknown[]; output_path?: string } | undefined; if (!spec) return { valid: false, reason: 'Missing spec parameter' }; if (!spec.slides || !Array.isArray(spec.slides)) { return { valid: false, reason: 'spec.slides must be an array' }; diff --git a/test/core-suite.test.ts b/test/core-suite.test.ts index ba9a43b..6fe9548 100644 --- a/test/core-suite.test.ts +++ b/test/core-suite.test.ts @@ -415,9 +415,403 @@ describe('AgentLoop', () => { }), observe: async (state: any, _result: any) => state, learn: async (_state: any) => {}, + shouldTerminate: (_state: any) => false, }; const loop = new mod.AgentLoop(minimalModules); assert.ok(loop); }); }); + +// ============================================================================ +// 7. Effect System +// ============================================================================ + +describe('Effect', () => { + test('succeed + runPromise', async () => { + const mod = await import('../dist/src/core/effect.js'); + const result = await mod.runPromise(mod.succeed(42)); + assert.strictEqual(result, 42); + }); + + test('fail + runPromise rejects', async () => { + const mod = await import('../dist/src/core/effect.js'); + await assert.rejects( + () => mod.runPromise(mod.fail(new mod.ToolError('boom'))), + (err: any) => err._tag === 'ToolError' && err.message === 'boom' + ); + }); + + test('succeed + runSync works', async () => { + const mod = await import('../dist/src/core/effect.js'); + const value = mod.runSync(mod.succeed('hello')); + assert.strictEqual(value, 'hello'); + }); + + test('sync + runSync works', async () => { + const mod = await import('../dist/src/core/effect.js'); + const value = mod.runSync(mod.sync(() => 1 + 2)); + assert.strictEqual(value, 3); + }); + + test('runSync throws on async effect', async () => { + const mod = await import('../dist/src/core/effect.js'); + const asyncEffect = mod.tryPromise( + () => Promise.resolve(42), + (e) => new mod.LLMError('fail', e), + ); + assert.throws(() => mod.runSync(asyncEffect as any), /synchronous evaluator/); + }); + + test('map transforms success value', async () => { + const mod = await import('../dist/src/core/effect.js'); + const doubled = mod.map(mod.succeed(21), (n: number) => n * 2); + const result = await mod.runPromise(doubled); + assert.strictEqual(result, 42); + }); + + test('flatMap chains effects', async () => { + const mod = await import('../dist/src/core/effect.js'); + const program = mod.flatMap( + mod.succeed(10), + (n: number) => mod.succeed(n * 3), + ); + const result = await mod.runPromise(program); + assert.strictEqual(result, 30); + }); + + test('catchAll recovers from failure', async () => { + const mod = await import('../dist/src/core/effect.js'); + const program = mod.catchAll( + mod.fail(new mod.ToolError('oops')), + (_err: any) => mod.succeed('recovered'), + ); + const result = await mod.runPromise(program); + assert.strictEqual(result, 'recovered'); + }); + + test('tryPromise catches rejection', async () => { + const mod = await import('../dist/src/core/effect.js'); + const program = mod.tryPromise( + () => Promise.reject(new Error('network')), + (cause) => new mod.LLMError('request failed', cause), + ); + const result = await mod.runEither(program); + assert.ok(mod.isLeft(result)); + assert.strictEqual(result.error._tag, 'LLMError'); + }); + + test('all runs concurrently and collects', async () => { + const mod = await import('../dist/src/core/effect.js'); + const results = await mod.runPromise(mod.all([ + mod.succeed(1), + mod.succeed(2), + mod.succeed(3), + ])); + assert.deepStrictEqual(results, [1, 2, 3]); + }); + + test('pipe composes left to right', async () => { + const mod = await import('../dist/src/core/effect.js'); + const result = mod.pipe( + 5, + (x: number) => x * 2, + (x: number) => x + 1, + ); + assert.strictEqual(result, 11); + }); + + test('runEither returns Right on success', async () => { + const mod = await import('../dist/src/core/effect.js'); + const result = await mod.runEither(mod.succeed(99)); + assert.ok(mod.isRight(result)); + assert.strictEqual(result.value, 99); + }); + + test('timeout fails slow effects', async () => { + const mod = await import('../dist/src/core/effect.js'); + const slow = mod.tryPromise( + () => new Promise(r => setTimeout(() => r('done'), 500)), + (e) => new mod.LLMError('fail', e), + ); + const bounded = mod.timeout(slow, 50); + const result = await mod.runEither(bounded); + assert.ok(mod.isLeft(result)); + assert.strictEqual((result.error as any)._tag, 'TimeoutError'); + }); + + test('GenesisError subtypes have correct _tag', async () => { + const mod = await import('../dist/src/core/effect.js'); + const llm = new mod.LLMError('test'); + const tool = new mod.ToolError('test'); + const mem = new mod.MemoryError('test'); + const bus = new mod.BusError('test'); + const cfg = new mod.ConfigError('test'); + const to = new mod.TimeoutError('test'); + + assert.strictEqual(llm._tag, 'LLMError'); + assert.strictEqual(tool._tag, 'ToolError'); + assert.strictEqual(mem._tag, 'MemoryError'); + assert.strictEqual(bus._tag, 'BusError'); + assert.strictEqual(cfg._tag, 'ConfigError'); + assert.strictEqual(to._tag, 'TimeoutError'); + + assert.ok(llm instanceof Error); + assert.ok(tool instanceof mod.GenesisError); + }); +}); + +// ============================================================================ +// 8. Budget Forcing +// ============================================================================ + +describe('BudgetForcing', () => { + test('estimateComplexity returns valid scores', async () => { + const mod = await import('../dist/src/thinking/budget-forcing.js'); + const simple = mod.estimateComplexity('hello'); + assert.ok(simple.score >= 0 && simple.score <= 1); + + const complex = mod.estimateComplexity( + 'Explain why the quantum field theory approach to consciousness ' + + 'requires analyzing the Hamiltonian, then compare with IIT, ' + + 'and finally derive the mathematical proof step by step' + ); + assert.ok(complex.score > simple.score); + }); + + test('allocateBudget distributes across phases', async () => { + const mod = await import('../dist/src/thinking/budget-forcing.js'); + const complexity = mod.estimateComplexity('analyze this problem'); + const budget = mod.allocateBudget(complexity, 4000); + assert.ok(budget.totalBudget === 4000); + assert.ok(budget.phases.length > 0); + assert.ok(budget.softCeiling > 0); + assert.ok(budget.hardFloor > 0); + }); + + test('shouldExtendThinking detects premature stops', async () => { + const mod = await import('../dist/src/thinking/budget-forcing.js'); + const complexity = mod.estimateComplexity('explain quantum computing step by step'); + const budget = mod.allocateBudget(complexity, 4000); + // Very few tokens used → should extend + const extend = mod.shouldExtendThinking(10, budget); + assert.strictEqual(extend, true); + }); + + test('shouldTruncateThinking detects over-generation', async () => { + const mod = await import('../dist/src/thinking/budget-forcing.js'); + const complexity = mod.estimateComplexity('hi'); + const budget = mod.allocateBudget(complexity, 500); + // Way past soft ceiling + high confidence → should truncate + const truncate = mod.shouldTruncateThinking(budget.softCeiling + 100, budget, 0.95); + assert.strictEqual(truncate, true); + }); +}); + +// ============================================================================ +// 9. Plan-and-Budget +// ============================================================================ + +describe('PlanAndBudget', () => { + test('PlanAndBudget class can plan and allocate', async () => { + const mod = await import('../dist/src/reasoning/plan-and-budget.js'); + const pab = new mod.PlanAndBudget(); + + const plan = pab.plan( + 'First explain the concept, then compare approaches, and finally recommend one', + 4000 + ); + + assert.ok(plan.subQuestions.length > 0); + assert.strictEqual(plan.totalBudget, 4000); + assert.ok(plan.synthesisReserve > 0); + + // Allocate + const allocs = pab.allocate(plan); + assert.ok(allocs.length > 0); + assert.ok(allocs.every((a: any) => a.tokens > 0)); + }); + + test('rebalance redistributes surplus', async () => { + const mod = await import('../dist/src/reasoning/plan-and-budget.js'); + const pab = new mod.PlanAndBudget(); + + const plan = pab.plan('Compare A with B, then analyze C', 3000); + + if (plan.subQuestions.length > 0) { + const results = [{ + subQuestionId: plan.subQuestions[0].id, + tokensUsed: Math.floor(plan.subQuestions[0].allocatedTokens * 0.3), + confidence: 0.9, + answer: 'test answer', + }]; + + const rebalanced = pab.rebalance(plan, results); + assert.ok(rebalanced.totalBudget === plan.totalBudget); + } + }); +}); + +// ============================================================================ +// 10. MAP-Elites Archive +// ============================================================================ + +describe('MAPElites', () => { + test('archive stores and retrieves elites', async () => { + const mod = await import('../dist/src/tool-factory/map-elites.js'); + const archive = new mod.MAPElitesArchive( + [ + { name: 'x', bins: 3, min: 0, max: 1 }, + { name: 'y', bins: 3, min: 0, max: 1 }, + ], + ); + + archive.add({ name: 'tool-a' }, [0.2, 0.8], 0.5); + archive.add({ name: 'tool-b' }, [0.9, 0.1], 0.9); + + assert.ok(archive.coverage() > 0); + assert.ok(archive.qdScore() > 0); + }); + + test('describeToolBehavior returns valid descriptors', async () => { + const mod = await import('../dist/src/tool-factory/map-elites.js'); + const mockTool = { + id: 'test-1', + name: 'test-tool', + description: 'A fast analysis tool for data', + version: 1, + status: 'candidate' as const, + source: 'function run(input) {\n if (input.x) {\n return input.x * 2;\n }\n return 0;\n}', + paramSchema: { + type: 'object', + properties: { + x: { type: 'number' }, + }, + description: 'Run analysis', + }, + createdBy: 'agent' as const, + createdFrom: 'test', + usageCount: 50, + successCount: 42, + failureCount: 8, + avgDuration: 200, + lastUsed: new Date(), + createdAt: new Date(), + }; + + const desc = mod.describeToolBehavior(mockTool); + assert.strictEqual(desc.length, 4); + assert.ok(desc.every((d: number) => d >= 0 && d <= 1)); + }); + + test('createToolArchive returns configured archive', async () => { + const mod = await import('../dist/src/tool-factory/map-elites.js'); + const archive = mod.createToolArchive(); + assert.ok(archive); + assert.strictEqual(archive.coverage(), 0); + }); +}); + +// ============================================================================ +// 11. Core Barrel Exports +// ============================================================================ + +describe('CoreBarrel', () => { + test('core/index.ts exports all modules', async () => { + const core = await import('../dist/src/core/index.js'); + + // Logger + assert.ok(typeof core.createLogger === 'function'); + assert.ok(typeof core.getLogger === 'function'); + + // Effect system + assert.ok(typeof core.succeed === 'function'); + assert.ok(typeof core.fail === 'function'); + assert.ok(typeof core.tryPromise === 'function'); + assert.ok(typeof core.runPromise === 'function'); + assert.ok(typeof core.runSync === 'function'); + assert.ok(typeof core.pipe === 'function'); + assert.ok(typeof core.map === 'function'); + assert.ok(typeof core.flatMap === 'function'); + assert.ok(core.GenesisError); + assert.ok(core.LLMError); + assert.ok(core.ToolError); + + // FEK Bridge + assert.ok(core.FEKBrainBridge); + + // Agent Loop + assert.ok(core.AgentLoop); + + // Bootstrap + assert.ok(typeof core.bootstrap === 'function'); + assert.ok(typeof core.resolve === 'function'); + assert.ok(typeof core.shutdown === 'function'); + + // Module Registry + assert.ok(core.ModuleRegistry); + assert.ok(typeof core.getModuleRegistry === 'function'); + }); + + test('effect system works via barrel import', async () => { + const { succeed, runSync, pipe } = await import('../dist/src/core/index.js'); + const result = runSync(succeed(42)); + assert.strictEqual(result, 42); + + const piped = pipe(10, (x: number) => x * 3, (x: number) => x + 2); + assert.strictEqual(piped, 32); + }); +}); + +// ============================================================================ +// 12. Typed Publisher +// ============================================================================ + +describe('TypedPublisher', () => { + test('createTypedPublisher and createTypedSubscriber are exported from bus', async () => { + const bus = await import('../dist/src/bus/index.js'); + assert.ok(typeof bus.createTypedPublisher === 'function'); + assert.ok(typeof bus.createTypedSubscriber === 'function'); + }); +}); + +// ============================================================================ +// 13. Module Registry +// ============================================================================ + +describe('ModuleRegistry', () => { + test('can instantiate registry', async () => { + const mod = await import('../dist/src/core/module-registry.js'); + const registry = new mod.ModuleRegistry(); + assert.ok(registry); + }); + + test('registers and queries modules', async () => { + const mod = await import('../dist/src/core/module-registry.js'); + const registry = new mod.ModuleRegistry(); + + registry.register( + 'test-module', + async () => ({ name: 'test' }), + { level: 1 }, + ); + + assert.ok(registry.get('test-module') === undefined); // not booted yet, but registered + assert.strictEqual(registry.get('nonexistent'), undefined); + }); + + test('boot initializes registered modules', async () => { + const mod = await import('../dist/src/core/module-registry.js'); + const registry = new mod.ModuleRegistry(); + + let initialized = false; + registry.register( + 'my-svc', + async () => { initialized = true; return {}; }, + { level: 1 }, + ); + + await registry.boot(1); + assert.strictEqual(initialized, true); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 86c2f96..0af516b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,5 +13,5 @@ "resolveJsonModule": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "test", "src/dashboard", "src/presentation/video/**/*.tsx", "src/core"] + "exclude": ["node_modules", "dist", "test", "src/dashboard", "src/presentation/video/**/*.tsx"] }