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:
22
AGENTS.md
22
AGENTS.md
@@ -46,6 +46,28 @@ The release script is also the required path for keeping both public npm package
|
|||||||
|
|
||||||
Only `@runfusion/fusion` is published. The others are internal workspace packages.
|
Only `@runfusion/fusion` is published. The others are internal workspace packages.
|
||||||
|
|
||||||
|
### Importing across `@fusion/*` packages
|
||||||
|
|
||||||
|
Because `@fusion/core`, `@fusion/dashboard`, and `@fusion/engine` are **not** on npm, the published CLI bundle (`packages/cli/dist/bin.js`) only works because tsup is configured with `noExternal: [/^@fusion\//]` — every `@fusion/*` import gets inlined into the bundle.
|
||||||
|
|
||||||
|
For that inlining to happen the import must be **statically analyzable**. The following anti-pattern silently breaks on the published `npm i -g @runfusion/fusion`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// ❌ BROKEN: variable specifier defeats static analysis
|
||||||
|
const engineModule = "@fusion/engine";
|
||||||
|
const engine = await import(/* @vite-ignore */ engineModule);
|
||||||
|
createFnAgent = engine.createFnAgent;
|
||||||
|
```
|
||||||
|
|
||||||
|
esbuild leaves the dynamic `import("@fusion/engine")` in the output, the package isn't installed at runtime, the import throws, the catch silently sets the binding to `undefined`, and the next call fails with a confusing TypeError like `createFnAgent2 is not a function` (issue Runfusion/Fusion#9, FN-2613). Affects every AI flow in the dashboard.
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
|
||||||
|
1. **Default to a static import** — `import { createFnAgent } from "@fusion/engine"` — so esbuild can bundle it and tests can `vi.mock("@fusion/engine", …)` it.
|
||||||
|
2. **The one exception is `@fusion/core` itself**, which can't statically import engine without a circular dependency (engine → core). Core uses dependency injection: `setCreateFnAgent` (in `packages/core/src/ai-engine-loader.ts`) is called by `packages/engine/src/index.ts` at module load. Don't add new dynamic `import("@fusion/engine")` calls in core — extend the loader instead.
|
||||||
|
3. **Never reintroduce the `engineModule = "@fusion/engine"` + `await import(/* @vite-ignore */ engineModule)` trick.** If you find one, treat it as a bug.
|
||||||
|
4. Test mocking still works with static imports — vitest's module-level `vi.mock("@fusion/engine", …)` hoists above the static import.
|
||||||
|
|
||||||
## Storage Model
|
## Storage Model
|
||||||
|
|
||||||
Fusion uses a hybrid storage architecture: structured metadata lives in SQLite (`.fusion/fusion.db`) while large blob files (PROMPT.md, attachments) remain on the filesystem under `.fusion/tasks/{ID}/`. The database runs in WAL mode for concurrent access.
|
Fusion uses a hybrid storage architecture: structured metadata lives in SQLite (`.fusion/fusion.db`) while large blob files (PROMPT.md, attachments) remain on the filesystem under `.fusion/tasks/{ID}/`. The database runs in WAL mode for concurrent access.
|
||||||
|
|||||||
@@ -1,45 +1,40 @@
|
|||||||
/**
|
/**
|
||||||
* Shared lazy loader for `@fusion/engine`'s `createFnAgent`.
|
* Shared lazy accessor for `@fusion/engine`'s `createFnAgent`.
|
||||||
*
|
*
|
||||||
* @fusion/engine must be imported dynamically (not statically) so that:
|
* Core can't import engine statically (engine depends on core, so a static
|
||||||
* - core can be consumed in test environments where engine isn't resolvable
|
* import would create a cycle). Instead, engine wires its `createFnAgent` in
|
||||||
* - a missing engine package fails soft instead of breaking module load
|
* via `setCreateFnAgent` when its module loads, and consumers in core read it
|
||||||
|
* back through `getFnAgent`.
|
||||||
*
|
*
|
||||||
* Using a variable module specifier also prevents bundlers (Vite) from
|
* If engine never loads (e.g. tests that only import core), `getFnAgent`
|
||||||
* statically analysing and trying to resolve the import at build time.
|
* returns `undefined` and callers degrade gracefully.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Engine exports a function type we intentionally don't pull in here — importing
|
// Engine exports a function type we intentionally don't pull in here — importing
|
||||||
// the type would reintroduce the static resolution this module is designed to avoid.
|
// the type would reintroduce the cycle this module is designed to avoid.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
type CreateFnAgent = any;
|
type CreateFnAgent = any;
|
||||||
|
|
||||||
let createFnAgent: CreateFnAgent | undefined;
|
let createFnAgent: CreateFnAgent | undefined;
|
||||||
|
|
||||||
async function initEngine(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const engineModule = "@fusion/engine";
|
|
||||||
const engine = await import(/* @vite-ignore */ engineModule);
|
|
||||||
createFnAgent = engine.createFnAgent;
|
|
||||||
} catch {
|
|
||||||
createFnAgent = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Shape of a message in an agent session's state. */
|
/** Shape of a message in an agent session's state. */
|
||||||
export interface AgentMessage {
|
export interface AgentMessage {
|
||||||
role: string;
|
role: string;
|
||||||
content?: string | Array<{ type: string; text: string }>;
|
content?: string | Array<{ type: string; text: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Promise that resolves once the initial load attempt has completed. */
|
/**
|
||||||
const engineReady: Promise<void> = initEngine();
|
* Wire engine's `createFnAgent` into core. Called by `@fusion/engine` at module
|
||||||
|
* load. Tests can also call this with a stub.
|
||||||
|
*/
|
||||||
|
export function setCreateFnAgent(fn: CreateFnAgent | undefined): void {
|
||||||
|
createFnAgent = fn;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns `createFnAgent` from `@fusion/engine`, or `undefined` if the engine
|
* Returns `createFnAgent` from `@fusion/engine`, or `undefined` if engine has
|
||||||
* could not be loaded (typical in tests or when engine isn't installed).
|
* not registered itself yet (typical in tests).
|
||||||
*/
|
*/
|
||||||
export async function getFnAgent(): Promise<CreateFnAgent> {
|
export async function getFnAgent(): Promise<CreateFnAgent> {
|
||||||
await engineReady;
|
|
||||||
return createFnAgent;
|
return createFnAgent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ export {
|
|||||||
getTemplatesForRole,
|
getTemplatesForRole,
|
||||||
} from "./agent-prompts.js";
|
} from "./agent-prompts.js";
|
||||||
|
|
||||||
|
// ── Engine wiring (set by @fusion/engine at module load) ────────────
|
||||||
|
export { setCreateFnAgent, getFnAgent, type AgentMessage } from "./ai-engine-loader.js";
|
||||||
|
|
||||||
// ── Prompt Overrides ─────────────────────────────────────────────────
|
// ── Prompt Overrides ─────────────────────────────────────────────────
|
||||||
export {
|
export {
|
||||||
PROMPT_KEY_CATALOG,
|
PROMPT_KEY_CATALOG,
|
||||||
|
|||||||
@@ -40,28 +40,13 @@ async function initPromptCatalog() {
|
|||||||
// Initialize prompt catalog (will be awaited in actual usage)
|
// Initialize prompt catalog (will be awaited in actual usage)
|
||||||
const promptCatalogReadyPromise = initPromptCatalog();
|
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
|
// 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)
|
function ensureEngineReady(): Promise<void> {
|
||||||
async function initEngine() {
|
return Promise.resolve();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Constants ───────────────────────────────────────────────────────────────
|
// ── Constants ───────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -14,29 +14,13 @@
|
|||||||
import type { PromptOverrideMap } from "@fusion/core";
|
import type { PromptOverrideMap } from "@fusion/core";
|
||||||
import { resolvePrompt } 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
|
// 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)
|
function ensureEngineReady(): Promise<void> {
|
||||||
async function initEngine() {
|
return Promise.resolve();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Types ───────────────────────────────────────────────────────────────────
|
// ── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -25,13 +25,17 @@ import { EventEmitter } from "node:events";
|
|||||||
import { join, resolve, relative } from "node:path";
|
import { join, resolve, relative } from "node:path";
|
||||||
import { SessionEventBuffer } from "./sse-buffer.js";
|
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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
type AgentResult = any;
|
type AgentResult = any;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let buildAgentChatPromptFn: any;
|
let buildAgentChatPromptFn: any = engineBuildAgentChatPrompt;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Diagnostics logger for the chat module.
|
* Diagnostics logger for the chat module.
|
||||||
@@ -93,35 +97,8 @@ const diagnostics: DiagnosticsLogger = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize the import (this runs in actual server, mocked in tests)
|
function ensureEngineReady(): Promise<void> {
|
||||||
async function initEngine() {
|
return Promise.resolve();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Constants ───────────────────────────────────────────────────────────────
|
// ── Constants ───────────────────────────────────────────────────────────────
|
||||||
@@ -912,8 +889,7 @@ export function __setBuildAgentChatPrompt(mock: typeof buildAgentChatPromptFn):
|
|||||||
export function __resetChatState(): void {
|
export function __resetChatState(): void {
|
||||||
chatStreamManager.reset();
|
chatStreamManager.reset();
|
||||||
rateLimits.clear();
|
rateLimits.clear();
|
||||||
engineReady = undefined;
|
buildAgentChatPromptFn = engineBuildAgentChatPrompt;
|
||||||
buildAgentChatPromptFn = undefined;
|
|
||||||
|
|
||||||
// Reset diagnostics logger to default
|
// Reset diagnostics logger to default
|
||||||
__setChatDiagnostics(null);
|
__setChatDiagnostics(null);
|
||||||
|
|||||||
@@ -94,29 +94,15 @@ function parseTargetInterviewResponseImpl(text: string): TargetInterviewResponse
|
|||||||
// Export the parse function for tests
|
// Export the parse function for tests
|
||||||
export { parseTargetInterviewResponseImpl as parseTargetInterviewResponse };
|
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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
type AgentResult = any;
|
type AgentResult = any;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let createFnAgent: any;
|
let createFnAgent: any = engineCreateFnAgent;
|
||||||
|
|
||||||
async function initEngine() {
|
function ensureEngineReady(): Promise<void> {
|
||||||
if (!createFnAgent) {
|
return Promise.resolve();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Constants ───────────────────────────────────────────────────────────────
|
// ── Constants ───────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -27,11 +27,12 @@ import {
|
|||||||
nonfatal,
|
nonfatal,
|
||||||
} from "./ai-session-diagnostics.js";
|
} 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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
type AgentResult = any;
|
type AgentResult = any;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-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.
|
* Shared diagnostics helper for the mission-interview module.
|
||||||
@@ -64,23 +65,8 @@ export function __setMissionInterviewDiagnostics(_logger: unknown): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initEngine() {
|
function ensureEngineReady(): Promise<void> {
|
||||||
if (!createFnAgent) {
|
return Promise.resolve();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Constants ───────────────────────────────────────────────────────────────
|
// ── Constants ───────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -30,12 +30,17 @@ import {
|
|||||||
resetDiagnosticsSink,
|
resetDiagnosticsSink,
|
||||||
nonfatal,
|
nonfatal,
|
||||||
} from "./ai-session-diagnostics.js";
|
} 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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
type AgentResult = any;
|
type AgentResult = any;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let createFnAgent: any;
|
let createFnAgent: any = engineCreateFnAgent;
|
||||||
|
|
||||||
interface PlanningNtfyConfig {
|
interface PlanningNtfyConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -58,8 +63,11 @@ interface PlanningNtfyHelpers {
|
|||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let planningNtfyHelpers: PlanningNtfyHelpers | undefined;
|
let planningNtfyHelpers: PlanningNtfyHelpers | undefined = {
|
||||||
let ntfyHelpersReady: Promise<void> | undefined;
|
isNtfyEventEnabled: engineIsNtfyEventEnabled,
|
||||||
|
buildNtfyClickUrl: engineBuildNtfyClickUrl,
|
||||||
|
sendNtfyNotification: engineSendNtfyNotification,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared diagnostics helper for the planning module.
|
* 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)
|
function ensureEngineReady(): Promise<void> {
|
||||||
async function initEngine() {
|
return Promise.resolve();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureNtfyHelpersReady(): Promise<void> {
|
async function ensureNtfyHelpersReady(): Promise<void> {
|
||||||
ntfyHelpersReady ??= initEngine();
|
// Helpers are bound statically at module load; nothing to await.
|
||||||
await ntfyHelpersReady;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Constants ───────────────────────────────────────────────────────────────
|
// ── Constants ───────────────────────────────────────────────────────────────
|
||||||
@@ -1712,8 +1693,11 @@ export function __resetPlanningState(): void {
|
|||||||
_aiSessionDeletedListener = undefined;
|
_aiSessionDeletedListener = undefined;
|
||||||
_aiSessionStore = undefined;
|
_aiSessionStore = undefined;
|
||||||
|
|
||||||
planningNtfyHelpers = undefined;
|
planningNtfyHelpers = {
|
||||||
ntfyHelpersReady = undefined;
|
isNtfyEventEnabled: engineIsNtfyEventEnabled,
|
||||||
|
buildNtfyClickUrl: engineBuildNtfyClickUrl,
|
||||||
|
sendNtfyNotification: engineSendNtfyNotification,
|
||||||
|
};
|
||||||
|
|
||||||
// Reset diagnostics sink to default
|
// Reset diagnostics sink to default
|
||||||
resetDiagnosticsSink();
|
resetDiagnosticsSink();
|
||||||
@@ -1729,7 +1713,6 @@ export function __setCreateFnAgent(mock: typeof createFnAgent): void {
|
|||||||
/** Inject ntfy helper implementations (test-only). */
|
/** Inject ntfy helper implementations (test-only). */
|
||||||
export function __setPlanningNtfyHelpers(mock: PlanningNtfyHelpers | undefined): void {
|
export function __setPlanningNtfyHelpers(mock: PlanningNtfyHelpers | undefined): void {
|
||||||
planningNtfyHelpers = mock;
|
planningNtfyHelpers = mock;
|
||||||
ntfyHelpersReady = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Custom Errors ───────────────────────────────────────────────────────────
|
// ── Custom Errors ───────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -13,38 +13,13 @@
|
|||||||
* - Error mapping (validation 400, not found 404, AI/parser 500/503)
|
* - 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
|
// 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> {
|
async function initEngine(): Promise<void> {
|
||||||
if (engineInitialized) return;
|
// Engine is statically imported; nothing to do.
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Types ───────────────────────────────────────────────────────────────────
|
// ── Types ───────────────────────────────────────────────────────────────────
|
||||||
@@ -893,9 +868,7 @@ export class ServiceUnavailableError extends Error {
|
|||||||
* Reset module state. Used for testing only.
|
* Reset module state. Used for testing only.
|
||||||
*/
|
*/
|
||||||
export function __resetSuggestionState(): void {
|
export function __resetSuggestionState(): void {
|
||||||
createFnAgent = undefined;
|
createFnAgent = engineCreateFnAgent;
|
||||||
engineInitialized = false;
|
|
||||||
createFnAgentExplicitlySet = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -903,5 +876,4 @@ export function __resetSuggestionState(): void {
|
|||||||
*/
|
*/
|
||||||
export function __setCreateFnAgent(mock: typeof createFnAgent): void {
|
export function __setCreateFnAgent(mock: typeof createFnAgent): void {
|
||||||
createFnAgent = mock;
|
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.
|
import { createFnAgent as engineCreateFnAgentForRefine } from "@fusion/engine";
|
||||||
let createFnAgentForRefine: typeof import("@fusion/engine").createFnAgent | undefined;
|
|
||||||
|
// 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. */
|
/** @internal Inject a mock createFnAgent function for workflow-step refine route tests. */
|
||||||
export function __setCreateFnAgentForRefine(mock: typeof createFnAgentForRefine): void {
|
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
|
// Use AI to refine the description into a detailed agent prompt
|
||||||
let refinedPrompt: string;
|
let refinedPrompt: string;
|
||||||
try {
|
try {
|
||||||
let createFnAgent = createFnAgentForRefine;
|
const 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 settings = await scopedStore.getSettings();
|
const settings = await scopedStore.getSettings();
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import {
|
|||||||
resetDiagnosticsSink,
|
resetDiagnosticsSink,
|
||||||
} from "./ai-session-diagnostics.js";
|
} from "./ai-session-diagnostics.js";
|
||||||
|
|
||||||
|
import { createFnAgent as engineCreateFnAgent } from "@fusion/engine";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let createFnAgent: any;
|
let createFnAgent: any = engineCreateFnAgent;
|
||||||
const engineModule = "@fusion/engine";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared diagnostics helper for the subtask-breakdown module.
|
* Shared diagnostics helper for the subtask-breakdown module.
|
||||||
@@ -45,21 +46,8 @@ export function __setSubtaskBreakdownDiagnostics(_logger: unknown): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initEngine() {
|
function ensureEngineReady(): Promise<void> {
|
||||||
if (!createFnAgent) {
|
return Promise.resolve();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SubtaskItem {
|
export interface SubtaskItem {
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ export { MissionExecutionLoop, type MissionExecutionLoopOptions, type Validation
|
|||||||
export { aiMergeTask, type MergerOptions } from "./merger.js";
|
export { aiMergeTask, type MergerOptions } from "./merger.js";
|
||||||
export { reviewStep, type ReviewType, type ReviewVerdict, type ReviewResult, type ReviewOptions } from "./reviewer.js";
|
export { reviewStep, type ReviewType, type ReviewVerdict, type ReviewResult, type ReviewOptions } from "./reviewer.js";
|
||||||
export { createFnAgent, promptWithFallback, describeModel, setHostExtensionPaths, getHostExtensionPaths, type AgentOptions, type AgentResult } from "./pi.js";
|
export { createFnAgent, promptWithFallback, describeModel, setHostExtensionPaths, getHostExtensionPaths, type AgentOptions, type AgentResult } from "./pi.js";
|
||||||
|
|
||||||
|
// Register createFnAgent into core's loader so consumers in @fusion/core
|
||||||
|
// (e.g. ai-summarize, memory-compaction) can resolve it without a circular
|
||||||
|
// static import. Runs once at engine module load.
|
||||||
|
import { setCreateFnAgent } from "@fusion/core";
|
||||||
|
import { createFnAgent as _createFnAgentForCore } from "./pi.js";
|
||||||
|
setCreateFnAgent(_createFnAgentForCore);
|
||||||
export {
|
export {
|
||||||
resolveSessionSkills,
|
resolveSessionSkills,
|
||||||
createSkillsOverrideFromSelection,
|
createSkillsOverrideFromSelection,
|
||||||
|
|||||||
Reference in New Issue
Block a user