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:
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user