fix(dashboard,core,engine): statically import @fusion/engine to fix createFnAgent undefined in published CLI
The dashboard modules used a variable-specifier dynamic import (`const m = "@fusion/engine"; await import(m)`) to defeat bundler static analysis. tsup honored that and left the dynamic import in dist/bin.js, so the published `@runfusion/fusion` package failed at runtime with "createFnAgent2 is not a function" — `@fusion/engine` isn't on npm and the silent catch set the binding to undefined. Replaces the trick with static imports across planning, chat, subtask-breakdown, mission-interview, agent-generation, ai-refine, roadmap-suggestions, milestone-slice-interview, and routes. Core can't statically import engine (cycle), so it now exposes setCreateFnAgent and engine wires itself in at module load. Documents the pattern in AGENTS.md. Fixes Runfusion/Fusion#9. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,28 +40,13 @@ async function initPromptCatalog() {
|
||||
// Initialize prompt catalog (will be awaited in actual usage)
|
||||
const promptCatalogReadyPromise = initPromptCatalog();
|
||||
|
||||
// Dynamic import for @fusion/engine to avoid resolution issues in test environment
|
||||
import { createFnAgent as engineCreateFnAgent } from "@fusion/engine";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let createFnAgent: any;
|
||||
let createFnAgent: any = engineCreateFnAgent;
|
||||
|
||||
// Initialize the import (this runs in actual server, mocked in tests)
|
||||
async function initEngine() {
|
||||
if (!createFnAgent) {
|
||||
try {
|
||||
const engineModule = "@fusion/engine";
|
||||
const engine = await import(/* @vite-ignore */ engineModule);
|
||||
createFnAgent = engine.createFnAgent;
|
||||
} catch {
|
||||
// Allow failure in test environments - agent functionality will be stubbed
|
||||
createFnAgent = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let engineReady: Promise<void> | undefined;
|
||||
function ensureEngineReady() {
|
||||
engineReady ??= initEngine();
|
||||
return engineReady;
|
||||
function ensureEngineReady(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// ── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -14,29 +14,13 @@
|
||||
import type { PromptOverrideMap } from "@fusion/core";
|
||||
import { resolvePrompt } from "@fusion/core";
|
||||
|
||||
// Dynamic import for @fusion/engine to avoid resolution issues in test environment
|
||||
import { createFnAgent as engineCreateFnAgent } from "@fusion/engine";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let createFnAgent: any;
|
||||
let createFnAgent: any = engineCreateFnAgent;
|
||||
|
||||
// Initialize the import (this runs in actual server, mocked in tests)
|
||||
async function initEngine() {
|
||||
if (!createFnAgent) {
|
||||
try {
|
||||
// Use dynamic import with variable to prevent static analysis
|
||||
const engineModule = "@fusion/engine";
|
||||
const engine = await import(/* @vite-ignore */ engineModule);
|
||||
createFnAgent = engine.createFnAgent;
|
||||
} catch {
|
||||
// Allow failure in test environments - agent functionality will be stubbed
|
||||
createFnAgent = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let engineReady: Promise<void> | undefined;
|
||||
function ensureEngineReady() {
|
||||
engineReady ??= initEngine();
|
||||
return engineReady;
|
||||
function ensureEngineReady(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// ── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -25,13 +25,17 @@ import { EventEmitter } from "node:events";
|
||||
import { join, resolve, relative } from "node:path";
|
||||
import { SessionEventBuffer } from "./sse-buffer.js";
|
||||
|
||||
// Dynamic import for @fusion/engine to avoid resolution issues in test environment
|
||||
import {
|
||||
createFnAgent as engineCreateFnAgent,
|
||||
buildAgentChatPrompt as engineBuildAgentChatPrompt,
|
||||
} from "@fusion/engine";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type AgentResult = any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let createFnAgent: any;
|
||||
let createFnAgent: any = engineCreateFnAgent;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let buildAgentChatPromptFn: any;
|
||||
let buildAgentChatPromptFn: any = engineBuildAgentChatPrompt;
|
||||
|
||||
/**
|
||||
* Diagnostics logger for the chat module.
|
||||
@@ -93,35 +97,8 @@ const diagnostics: DiagnosticsLogger = {
|
||||
},
|
||||
};
|
||||
|
||||
// Initialize the import (this runs in actual server, mocked in tests)
|
||||
async function initEngine() {
|
||||
if (!createFnAgent || !buildAgentChatPromptFn) {
|
||||
try {
|
||||
// Use dynamic import with variable to prevent static analysis
|
||||
const engineModule = "@fusion/engine";
|
||||
const engine = await import(/* @vite-ignore */ engineModule);
|
||||
if (!createFnAgent) {
|
||||
createFnAgent = engine.createFnAgent;
|
||||
}
|
||||
if (!buildAgentChatPromptFn) {
|
||||
buildAgentChatPromptFn = engine.buildAgentChatPrompt;
|
||||
}
|
||||
} catch {
|
||||
// Allow failure in test environments - agent functionality will be stubbed
|
||||
if (!createFnAgent) {
|
||||
createFnAgent = undefined;
|
||||
}
|
||||
if (!buildAgentChatPromptFn) {
|
||||
buildAgentChatPromptFn = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let engineReady: Promise<void> | undefined;
|
||||
function ensureEngineReady() {
|
||||
engineReady ??= initEngine();
|
||||
return engineReady;
|
||||
function ensureEngineReady(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// ── Constants ───────────────────────────────────────────────────────────────
|
||||
@@ -912,8 +889,7 @@ export function __setBuildAgentChatPrompt(mock: typeof buildAgentChatPromptFn):
|
||||
export function __resetChatState(): void {
|
||||
chatStreamManager.reset();
|
||||
rateLimits.clear();
|
||||
engineReady = undefined;
|
||||
buildAgentChatPromptFn = undefined;
|
||||
buildAgentChatPromptFn = engineBuildAgentChatPrompt;
|
||||
|
||||
// Reset diagnostics logger to default
|
||||
__setChatDiagnostics(null);
|
||||
|
||||
@@ -94,29 +94,15 @@ function parseTargetInterviewResponseImpl(text: string): TargetInterviewResponse
|
||||
// Export the parse function for tests
|
||||
export { parseTargetInterviewResponseImpl as parseTargetInterviewResponse };
|
||||
|
||||
// Dynamic import for @fusion/engine to avoid resolution issues in test environment
|
||||
import { createFnAgent as engineCreateFnAgent } from "@fusion/engine";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type AgentResult = any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let createFnAgent: any;
|
||||
let createFnAgent: any = engineCreateFnAgent;
|
||||
|
||||
async function initEngine() {
|
||||
if (!createFnAgent) {
|
||||
try {
|
||||
const engineModule = "@fusion/engine";
|
||||
const engine = await import(/* @vite-ignore */ engineModule);
|
||||
createFnAgent = engine.createFnAgent;
|
||||
} catch {
|
||||
// Allow failure in test environments
|
||||
createFnAgent = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let engineReady: Promise<void> | undefined;
|
||||
function ensureEngineReady() {
|
||||
engineReady ??= initEngine();
|
||||
return engineReady;
|
||||
function ensureEngineReady(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// ── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -27,11 +27,12 @@ import {
|
||||
nonfatal,
|
||||
} from "./ai-session-diagnostics.js";
|
||||
|
||||
// Dynamic import for @fusion/engine to avoid resolution issues in test environment
|
||||
import { createFnAgent as engineCreateFnAgent } from "@fusion/engine";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type AgentResult = any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let createFnAgent: any;
|
||||
let createFnAgent: any = engineCreateFnAgent;
|
||||
|
||||
/**
|
||||
* Shared diagnostics helper for the mission-interview module.
|
||||
@@ -64,23 +65,8 @@ export function __setMissionInterviewDiagnostics(_logger: unknown): void {
|
||||
}
|
||||
}
|
||||
|
||||
async function initEngine() {
|
||||
if (!createFnAgent) {
|
||||
try {
|
||||
const engineModule = "@fusion/engine";
|
||||
const engine = await import(/* @vite-ignore */ engineModule);
|
||||
createFnAgent = engine.createFnAgent;
|
||||
} catch {
|
||||
// Allow failure in test environments
|
||||
createFnAgent = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let engineReady: Promise<void> | undefined;
|
||||
function ensureEngineReady() {
|
||||
engineReady ??= initEngine();
|
||||
return engineReady;
|
||||
function ensureEngineReady(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// ── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -30,12 +30,17 @@ import {
|
||||
resetDiagnosticsSink,
|
||||
nonfatal,
|
||||
} from "./ai-session-diagnostics.js";
|
||||
import {
|
||||
createFnAgent as engineCreateFnAgent,
|
||||
isNtfyEventEnabled as engineIsNtfyEventEnabled,
|
||||
buildNtfyClickUrl as engineBuildNtfyClickUrl,
|
||||
sendNtfyNotification as engineSendNtfyNotification,
|
||||
} from "@fusion/engine";
|
||||
|
||||
// Dynamic import for @fusion/engine to avoid resolution issues in test environment
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type AgentResult = any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let createFnAgent: any;
|
||||
let createFnAgent: any = engineCreateFnAgent;
|
||||
|
||||
interface PlanningNtfyConfig {
|
||||
enabled: boolean;
|
||||
@@ -58,8 +63,11 @@ interface PlanningNtfyHelpers {
|
||||
}) => Promise<void>;
|
||||
}
|
||||
|
||||
let planningNtfyHelpers: PlanningNtfyHelpers | undefined;
|
||||
let ntfyHelpersReady: Promise<void> | undefined;
|
||||
let planningNtfyHelpers: PlanningNtfyHelpers | undefined = {
|
||||
isNtfyEventEnabled: engineIsNtfyEventEnabled,
|
||||
buildNtfyClickUrl: engineBuildNtfyClickUrl,
|
||||
sendNtfyNotification: engineSendNtfyNotification,
|
||||
};
|
||||
|
||||
/**
|
||||
* Shared diagnostics helper for the planning module.
|
||||
@@ -92,39 +100,12 @@ export function __setPlanningDiagnostics(_logger: unknown): void {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the import (this runs in actual server, mocked in tests)
|
||||
async function initEngine() {
|
||||
try {
|
||||
// Use dynamic import with variable to prevent static analysis
|
||||
const engineModule = "@fusion/engine";
|
||||
const engine = await import(/* @vite-ignore */ engineModule);
|
||||
if (!createFnAgent) {
|
||||
createFnAgent = engine.createFnAgent;
|
||||
}
|
||||
if (!planningNtfyHelpers) {
|
||||
planningNtfyHelpers = {
|
||||
isNtfyEventEnabled: engine.isNtfyEventEnabled,
|
||||
buildNtfyClickUrl: engine.buildNtfyClickUrl,
|
||||
sendNtfyNotification: engine.sendNtfyNotification,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Allow failure in test environments - agent functionality will be stubbed
|
||||
if (!createFnAgent) {
|
||||
createFnAgent = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let engineReady: Promise<void> | undefined;
|
||||
function ensureEngineReady() {
|
||||
engineReady ??= initEngine();
|
||||
return engineReady;
|
||||
function ensureEngineReady(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async function ensureNtfyHelpersReady(): Promise<void> {
|
||||
ntfyHelpersReady ??= initEngine();
|
||||
await ntfyHelpersReady;
|
||||
// Helpers are bound statically at module load; nothing to await.
|
||||
}
|
||||
|
||||
// ── Constants ───────────────────────────────────────────────────────────────
|
||||
@@ -1712,8 +1693,11 @@ export function __resetPlanningState(): void {
|
||||
_aiSessionDeletedListener = undefined;
|
||||
_aiSessionStore = undefined;
|
||||
|
||||
planningNtfyHelpers = undefined;
|
||||
ntfyHelpersReady = undefined;
|
||||
planningNtfyHelpers = {
|
||||
isNtfyEventEnabled: engineIsNtfyEventEnabled,
|
||||
buildNtfyClickUrl: engineBuildNtfyClickUrl,
|
||||
sendNtfyNotification: engineSendNtfyNotification,
|
||||
};
|
||||
|
||||
// Reset diagnostics sink to default
|
||||
resetDiagnosticsSink();
|
||||
@@ -1729,7 +1713,6 @@ export function __setCreateFnAgent(mock: typeof createFnAgent): void {
|
||||
/** Inject ntfy helper implementations (test-only). */
|
||||
export function __setPlanningNtfyHelpers(mock: PlanningNtfyHelpers | undefined): void {
|
||||
planningNtfyHelpers = mock;
|
||||
ntfyHelpersReady = undefined;
|
||||
}
|
||||
|
||||
// ── Custom Errors ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -13,38 +13,13 @@
|
||||
* - Error mapping (validation 400, not found 404, AI/parser 500/503)
|
||||
*/
|
||||
|
||||
// Dynamic import for @fusion/engine to avoid resolution issues in test environment
|
||||
import { createFnAgent as engineCreateFnAgent } from "@fusion/engine";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let createFnAgent: any;
|
||||
let createFnAgent: any = engineCreateFnAgent;
|
||||
|
||||
// Track if engine has been initialized (prevents multiple imports)
|
||||
let engineInitialized = false;
|
||||
|
||||
// Flag to indicate if createFnAgent was explicitly set (even to undefined)
|
||||
let createFnAgentExplicitlySet = false;
|
||||
|
||||
// Initialize the import (this runs in actual server, mocked in tests)
|
||||
async function initEngine(): Promise<void> {
|
||||
if (engineInitialized) return;
|
||||
|
||||
// If createFnAgent was explicitly set (even to undefined), don't try to import
|
||||
if (createFnAgentExplicitlySet) {
|
||||
engineInitialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!createFnAgent) {
|
||||
try {
|
||||
// Use dynamic import with variable to prevent static analysis
|
||||
const engineModule = "@fusion/engine";
|
||||
const engine = await import(/* @vite-ignore */ engineModule);
|
||||
createFnAgent = engine.createFnAgent;
|
||||
} catch {
|
||||
// Allow failure in test environments - agent functionality will be stubbed
|
||||
createFnAgent = undefined;
|
||||
}
|
||||
}
|
||||
engineInitialized = true;
|
||||
// Engine is statically imported; nothing to do.
|
||||
}
|
||||
|
||||
// ── Types ───────────────────────────────────────────────────────────────────
|
||||
@@ -893,9 +868,7 @@ export class ServiceUnavailableError extends Error {
|
||||
* Reset module state. Used for testing only.
|
||||
*/
|
||||
export function __resetSuggestionState(): void {
|
||||
createFnAgent = undefined;
|
||||
engineInitialized = false;
|
||||
createFnAgentExplicitlySet = false;
|
||||
createFnAgent = engineCreateFnAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -903,5 +876,4 @@ export function __resetSuggestionState(): void {
|
||||
*/
|
||||
export function __setCreateFnAgent(mock: typeof createFnAgent): void {
|
||||
createFnAgent = mock;
|
||||
createFnAgentExplicitlySet = true;
|
||||
}
|
||||
|
||||
@@ -218,8 +218,10 @@ async function discoverDashboardPiExtensions(cwd: string): Promise<PiExtensionSe
|
||||
};
|
||||
}
|
||||
|
||||
// Dynamic import fallback for @fusion/engine with injectable override for tests.
|
||||
let createFnAgentForRefine: typeof import("@fusion/engine").createFnAgent | undefined;
|
||||
import { createFnAgent as engineCreateFnAgentForRefine } from "@fusion/engine";
|
||||
|
||||
// Test-injectable override; defaults to the statically imported engine binding.
|
||||
let createFnAgentForRefine: typeof import("@fusion/engine").createFnAgent | undefined = engineCreateFnAgentForRefine;
|
||||
|
||||
/** @internal Inject a mock createFnAgent function for workflow-step refine route tests. */
|
||||
export function __setCreateFnAgentForRefine(mock: typeof createFnAgentForRefine): void {
|
||||
@@ -2513,13 +2515,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
|
||||
// Use AI to refine the description into a detailed agent prompt
|
||||
let refinedPrompt: string;
|
||||
try {
|
||||
let createFnAgent = createFnAgentForRefine;
|
||||
if (!createFnAgent) {
|
||||
// Dynamic import to avoid resolution issues in tests
|
||||
const engineModule = "@fusion/engine";
|
||||
const engine = await import(/* @vite-ignore */ engineModule);
|
||||
createFnAgent = engine.createFnAgent;
|
||||
}
|
||||
const createFnAgent = createFnAgentForRefine;
|
||||
|
||||
const settings = await scopedStore.getSettings();
|
||||
|
||||
|
||||
@@ -9,9 +9,10 @@ import {
|
||||
resetDiagnosticsSink,
|
||||
} from "./ai-session-diagnostics.js";
|
||||
|
||||
import { createFnAgent as engineCreateFnAgent } from "@fusion/engine";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let createFnAgent: any;
|
||||
const engineModule = "@fusion/engine";
|
||||
let createFnAgent: any = engineCreateFnAgent;
|
||||
|
||||
/**
|
||||
* Shared diagnostics helper for the subtask-breakdown module.
|
||||
@@ -45,21 +46,8 @@ export function __setSubtaskBreakdownDiagnostics(_logger: unknown): void {
|
||||
}
|
||||
}
|
||||
|
||||
async function initEngine() {
|
||||
if (!createFnAgent) {
|
||||
try {
|
||||
const engine = await import(/* @vite-ignore */ engineModule);
|
||||
createFnAgent = engine.createFnAgent;
|
||||
} catch {
|
||||
createFnAgent = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let engineReady: Promise<void> | undefined;
|
||||
function ensureEngineReady() {
|
||||
engineReady ??= initEngine();
|
||||
return engineReady;
|
||||
function ensureEngineReady(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
export interface SubtaskItem {
|
||||
|
||||
Reference in New Issue
Block a user