feat(FN-2992): merge fusion/fn-2992

Commits merged:
- fix(FN-2992): align ResearchView styles with design tokens
- fix(FN-2992): align Research view with standalone layout conventions
- test(FN-2992): complete Step 6 — expand orchestration verification
- feat(FN-2992): complete Step 5 — wire research settings and engine exports
- test(FN-2992): cover provider error classification in step runner
- feat(FN-2992): complete Step 4 — implement research step runner
- test(FN-2992): add orchestrator lifecycle regression coverage
- feat(FN-2992): complete Step 3 — add research orchestrator lifecycle
- feat(FN-2992): complete Step 2 — align research store API events
- feat(FN-2992): complete Step 1 — define orchestration domain types
- feat(FN-2992): complete Step 1 — define orchestration domain types

Files changed:
packages/core/src/index.ts                         |  15 +
 packages/core/src/research-store.ts                |   8 +-
 packages/core/src/research-types.ts                | 109 +++++
 packages/core/src/settings-schema.ts               |  10 +
 packages/core/src/types.ts                         |  31 ++
 packages/dashboard/app/components/ResearchView.css |  87 +++-
 packages/dashboard/app/components/ResearchView.tsx | 144 +++---
 .../app/components/__tests__/ResearchView.test.tsx |   3 +-
 .../src/__tests__/research-orchestrator.test.ts    | 256 ++++++++++
 .../src/__tests__/research-step-runner.test.ts     | 102 ++++
 packages/engine/src/index.ts                       |  11 +
 packages/engine/src/project-engine.ts              |  17 +
 packages/engine/src/research-orchestrator.ts       | 517 +++++++++++++++++++++
 packages/engine/src/research-step-runner.ts        | 235 ++++++++++
 14 files changed, 1464 insertions(+), 81 deletions(-)

Fusion-Task-Id: FN-2992
This commit is contained in:
Fusion
2026-04-30 00:47:01 -07:00
committed by gsxdsm
parent b359cb8672
commit a227d5b305
14 changed files with 1466 additions and 83 deletions

View File

@@ -585,6 +585,8 @@ export {
RESEARCH_EXPORT_FORMATS,
RESEARCH_SOURCE_TYPES,
RESEARCH_EVENT_TYPES,
RESEARCH_ORCHESTRATION_PHASES,
RESEARCH_ORCHESTRATION_STEP_STATUSES,
} from "./research-types.js";
export type {
ResearchRunStatus,
@@ -603,6 +605,19 @@ export type {
ResearchRunUpdateInput,
ResearchRunListOptions,
ResearchStoreEvents,
ResearchOrchestrationPhase,
ResearchOrchestrationStepStatus,
ResearchOrchestrationStepType,
ResearchOrchestrationStep,
ResearchOrchestrationEventType,
ResearchOrchestrationEvent,
ResearchProviderConfig,
ResearchOrchestrationProvider,
ResearchModelSettings,
ResearchOrchestrationConfig,
ResearchSynthesisRequest,
ResearchSynthesisResult,
ResearchCancellationState,
} from "./research-types.js";
export { TodoStore } from "./todo-store.js";

View File

@@ -168,7 +168,7 @@ export class ResearchStore extends EventEmitter<ResearchStoreEvents> {
return deleted;
}
appendEvent(runId: string, event: Omit<ResearchEvent, "id" | "timestamp">): ResearchEvent {
addEvent(runId: string, event: Omit<ResearchEvent, "id" | "timestamp">): ResearchEvent {
const run = this.getRun(runId);
if (!run) throw new Error(`Research run not found: ${runId}`);
@@ -181,15 +181,21 @@ export class ResearchStore extends EventEmitter<ResearchStoreEvents> {
};
this.updateRun(runId, { events: [...run.events, created] });
this.emit("event:added", { runId, event: created });
return created;
}
appendEvent(runId: string, event: Omit<ResearchEvent, "id" | "timestamp">): ResearchEvent {
return this.addEvent(runId, event);
}
addSource(runId: string, source: Omit<ResearchSource, "id">): ResearchSource {
const run = this.getRun(runId);
if (!run) throw new Error(`Research run not found: ${runId}`);
const created: ResearchSource = { ...source, id: generateId("RSRC") };
this.updateRun(runId, { sources: [...run.sources, created] });
this.emit("source:added", { runId, source: created });
return created;
}

View File

@@ -159,4 +159,113 @@ export interface ResearchStoreEvents {
"run:completed": [ResearchRun];
"run:failed": [ResearchRun];
"run:cancelled": [ResearchRun];
"event:added": [{ runId: string; event: ResearchEvent }];
"source:added": [{ runId: string; source: ResearchSource }];
}
export const RESEARCH_ORCHESTRATION_PHASES = [
"planning",
"searching",
"fetching",
"synthesizing",
"finalizing",
"completed",
"failed",
"cancelled",
] as const;
export type ResearchOrchestrationPhase = typeof RESEARCH_ORCHESTRATION_PHASES[number];
export const RESEARCH_ORCHESTRATION_STEP_STATUSES = ["pending", "running", "completed", "failed", "skipped"] as const;
export type ResearchOrchestrationStepStatus = typeof RESEARCH_ORCHESTRATION_STEP_STATUSES[number];
export type ResearchOrchestrationStepType = "source-query" | "content-fetch" | "synthesis-pass";
export interface ResearchOrchestrationStep {
id: string;
type: ResearchOrchestrationStepType;
phase: ResearchOrchestrationPhase;
status: ResearchOrchestrationStepStatus;
order: number;
name: string;
input?: Record<string, unknown>;
output?: Record<string, unknown>;
error?: string;
startedAt?: string;
completedAt?: string;
}
export type ResearchOrchestrationEventType =
| "phase-changed"
| "step-started"
| "step-completed"
| "step-failed"
| "source-found"
| "synthesis-progress"
| "run-cancelled";
export interface ResearchOrchestrationEvent {
type: ResearchOrchestrationEventType;
phase: ResearchOrchestrationPhase;
message: string;
stepId?: string;
timestamp: string;
metadata?: Record<string, unknown>;
}
export interface ResearchProviderConfig {
timeoutMs?: number;
rateLimitPerMinute?: number;
maxResults?: number;
metadata?: Record<string, unknown>;
}
export interface ResearchOrchestrationProvider {
type: string;
config?: ResearchProviderConfig;
}
export interface ResearchModelSettings {
provider?: string;
modelId?: string;
temperature?: number;
maxTokens?: number;
timeoutMs?: number;
}
export interface ResearchOrchestrationConfig {
providers: ResearchOrchestrationProvider[];
maxSources: number;
maxSynthesisRounds: number;
phaseTimeoutMs?: number;
stepTimeoutMs?: number;
rateLimitPerMinute?: number;
synthesisModel?: ResearchModelSettings;
metadata?: Record<string, unknown>;
}
export interface ResearchSynthesisRequest {
query: string;
sources: ResearchSource[];
round: number;
desiredFormat?: "markdown" | "json" | "bullets";
instructions?: string;
}
export interface ResearchSynthesisResult {
output: string;
citations: string[];
confidence?: number;
usage?: ResearchTokenUsage;
metadata?: Record<string, unknown>;
}
export interface ResearchCancellationState {
runId: string;
controller: AbortController;
requestedAt: string;
acknowledgedAt?: string;
gracefulShutdown: boolean;
reason?: string;
}

View File

@@ -64,6 +64,11 @@ export const DEFAULT_GLOBAL_SETTINGS = {
// Dashboard TUI memory guard
vitestAutoKillEnabled: true,
vitestKillThresholdPct: 90,
researchGlobalEnabled: true,
researchGlobalMaxConcurrentRuns: 3,
researchGlobalDefaultTimeout: 300000,
researchGlobalMaxSourcesPerRun: 20,
researchGlobalMaxSynthesisRounds: 2,
} satisfies CompleteSettings<GlobalSettings>;
/** Default values for project-level settings. */
@@ -212,6 +217,11 @@ export const DEFAULT_PROJECT_SETTINGS = {
reflectionAfterTask: true,
reviewHandoffPolicy: "disabled",
showQuickChatFAB: false,
researchEnabled: true,
researchMaxConcurrentRuns: 3,
researchDefaultTimeout: 300000,
researchMaxSourcesPerRun: 20,
researchMaxSynthesisRounds: 2,
experimentalFeatures: {},
} satisfies CompleteSettings<ProjectSettings>;

View File

@@ -1389,6 +1389,22 @@ export interface GlobalSettings {
* triggers a vitest auto-kill. Clamped to [50, 99] in the UI.
* Default: 90. */
vitestKillThresholdPct?: number;
/** Enable or disable the research subsystem globally.
* When false, dashboard/API entrypoints should reject new research runs.
* Default: true when research store exists. */
researchGlobalEnabled?: boolean;
/** Maximum concurrent research runs allowed by default.
* Default: 3. */
researchGlobalMaxConcurrentRuns?: number;
/** Default timeout for end-to-end research runs in milliseconds.
* Default: 300000 (5 minutes). */
researchGlobalDefaultTimeout?: number;
/** Default maximum number of sources the orchestrator may fetch per run.
* Default: 20. */
researchGlobalMaxSourcesPerRun?: number;
/** Default maximum number of synthesis rounds per run.
* Default: 2. */
researchGlobalMaxSynthesisRounds?: number;
}
export type RemoteAccessProvider = "tailscale" | "cloudflare";
@@ -1514,6 +1530,21 @@ export interface ProjectSettings {
* - "block": prevent execution until the selected node is healthy/available (default)
* - "fallback-local": run on the local node when the selected node is unavailable */
unavailableNodePolicy?: UnavailableNodePolicy;
/** Enable or disable the research subsystem for this project.
* When undefined, falls back to global settings. */
researchEnabled?: boolean;
/** Project-level maximum concurrent research runs.
* When undefined, falls back to global settings (default 3). */
researchMaxConcurrentRuns?: number;
/** Project-level default run timeout in milliseconds.
* When undefined, falls back to global settings (default 300000). */
researchDefaultTimeout?: number;
/** Project-level source fetch cap per run.
* When undefined, falls back to global settings (default 20). */
researchMaxSourcesPerRun?: number;
/** Project-level synthesis round cap per run.
* When undefined, falls back to global settings (default 2). */
researchMaxSynthesisRounds?: number;
/** ID of the pinned default execution node. Tasks without a per-task override run on this node. */
defaultNodeId?: string;
/** Shell command to run inside each new worktree immediately after creation.