feat(FN-3025): add agent onboarding flow with launch gating

- Add backend onboarding session service and legacy API routes for start, state, and import/export generation
- Introduce experimental AgentOnboardingModal UI with stream-state handling, model selector wiring, and onboarding launch flow in AgentsView
- Add onboarding gating via prompt overrides/store updates and include search behavior fix for partial-match filtering
- Expand dashboard/core tests for onboarding API, modal behavior, agents/settings integration, and prompt override handling

Fusion-Task-Id: FN-3025
This commit is contained in:
Fusion
2026-04-30 21:20:31 -07:00
committed by gsxdsm
parent 4887a113b9
commit 2bbcadac6f
15 changed files with 1218 additions and 8 deletions

View File

@@ -2456,6 +2456,30 @@ export type PlanningStreamEvent =
| { type: "error"; data: string }
| { type: "complete"; data: Record<string, never> };
export interface AgentOnboardingSummary {
name: string;
role: AgentCapability | "custom";
instructionsText: string;
thinkingLevel: "off" | "minimal" | "low" | "medium" | "high";
maxTurns: number;
title?: string;
icon?: string;
reportsTo?: string;
soul?: string;
memory?: string;
skills?: string[];
templateId?: string;
patternAgentId?: string;
rationale?: string;
}
export type AgentOnboardingStreamEvent =
| { type: "thinking"; data: string }
| { type: "question"; data: PlanningQuestion }
| { type: "summary"; data: AgentOnboardingSummary }
| { type: "error"; data: string }
| { type: "complete"; data: Record<string, never> };
/** Start a new planning session with an initial plan */
export function startPlanning(initialPlan: string, projectId?: string): Promise<PlanningSession> {
return api<PlanningSession>(withProjectId("/planning/start", projectId), {
@@ -2531,6 +2555,56 @@ export function cancelPlanning(sessionId: string, projectId?: string, tabId?: st
});
}
export function startAgentOnboardingStreaming(
intent: string,
context: {
existingAgents: Array<{ id: string; name: string; role: string }>;
templates: Array<{ id: string; label: string; description?: string }>;
},
projectId?: string,
modelOverride?: { planningModelProvider?: string; planningModelId?: string },
): Promise<{ sessionId: string }> {
return api<{ sessionId: string }>(withProjectId("/agents/onboarding/start-streaming", projectId), {
method: "POST",
body: JSON.stringify({
intent,
context,
planningModelProvider: modelOverride?.planningModelProvider,
planningModelId: modelOverride?.planningModelId,
}),
});
}
export function respondToAgentOnboarding(
sessionId: string,
responses: Record<string, unknown>,
projectId?: string,
): Promise<{ type: "question" | "complete"; data: PlanningQuestion | AgentOnboardingSummary }> {
return api(withProjectId("/agents/onboarding/respond", projectId), {
method: "POST",
body: JSON.stringify({ sessionId, responses }),
});
}
export function retryAgentOnboardingSession(sessionId: string, projectId?: string): Promise<{ success: boolean; sessionId: string }> {
return api(withProjectId(`/agents/onboarding/${encodeURIComponent(sessionId)}/retry`, projectId), {
method: "POST",
});
}
export function stopAgentOnboardingGeneration(sessionId: string, projectId?: string): Promise<{ success: boolean }> {
return api(withProjectId(`/agents/onboarding/${encodeURIComponent(sessionId)}/stop`, projectId), {
method: "POST",
});
}
export function cancelAgentOnboarding(sessionId: string, projectId?: string): Promise<void> {
return api(withProjectId("/agents/onboarding/cancel", projectId), {
method: "POST",
body: JSON.stringify({ sessionId }),
});
}
/** Create a task from a completed planning session */
export function createTaskFromPlanning(
sessionId: string,
@@ -3613,6 +3687,63 @@ export function getPlanningStreamUrl(sessionId: string, projectId?: string): str
return buildApiUrl(withProjectId(`/planning/${encodeURIComponent(sessionId)}/stream`, projectId));
}
export function getAgentOnboardingStreamUrl(sessionId: string, projectId?: string): string {
return buildApiUrl(withProjectId(`/agents/onboarding/${encodeURIComponent(sessionId)}/stream`, projectId));
}
export function connectAgentOnboardingStream(
sessionId: string,
projectId: string | undefined,
handlers: {
onThinking?: (data: string) => void;
onQuestion?: (data: PlanningQuestion) => void;
onSummary?: (data: AgentOnboardingSummary) => void;
onError?: (data: string) => void;
onComplete?: () => void;
onConnectionStateChange?: (state: StreamConnectionState) => void;
},
options?: { maxReconnectAttempts?: number },
): { close: () => void; isConnected: () => boolean } {
const url = getAgentOnboardingStreamUrl(sessionId, projectId);
const resilient = createResilientEventSource(
url,
{
events: {
thinking: (event) => {
try { handlers.onThinking?.(JSON.parse(event.data)); } catch { handlers.onThinking?.(event.data); }
},
question: (event) => {
try { handlers.onQuestion?.(JSON.parse(event.data) as PlanningQuestion); } catch {}
},
summary: (event) => {
try { handlers.onSummary?.(JSON.parse(event.data) as AgentOnboardingSummary); } catch {}
},
error: (event) => {
try {
const parsed = JSON.parse(event.data);
handlers.onError?.(parsed.message || parsed);
} catch {
handlers.onError?.(event.data || "Stream error");
}
},
complete: () => {
handlers.onComplete?.();
},
},
},
{
maxReconnectAttempts: options?.maxReconnectAttempts,
onConnectionStateChange: handlers.onConnectionStateChange,
onFatalError: (message) => handlers.onError?.(message),
},
);
return {
close: resilient.close,
isConnected: resilient.isConnected,
};
}
/** Connect to planning session SSE stream and handle events
*
* Returns an object with: