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

This commit is contained in:
gsxdsm
2026-04-12 19:16:21 -07:00
parent 745c9c7ff4
commit aa6056d095
10 changed files with 219 additions and 12 deletions

View File

@@ -2377,6 +2377,8 @@ describe("Mission API", () => {
"Scoped Mission",
scopedRootDir,
{ "mission-interview-system": "Scoped mission interview prompt" },
undefined,
undefined,
);
});
@@ -2462,6 +2464,8 @@ describe("Mission API", () => {
"Default Mission",
"/fake/root",
{},
undefined,
undefined,
);
});

View File

@@ -196,6 +196,9 @@ interface MissionInterviewSession {
thinkingOutput: string;
/** Thinking output generated while producing currentQuestion */
lastGeneratedThinking: string;
/** Model override for this interview session */
modelProvider?: string;
modelId?: string;
createdAt: Date;
updatedAt: Date;
}
@@ -274,6 +277,8 @@ function persistMissionSession(session: MissionInterviewSession, status: "genera
ip: session.ip,
missionTitle: session.missionTitle,
missionId: session.missionId,
modelProvider: session.modelProvider,
modelId: session.modelId,
}),
conversationHistory: JSON.stringify(session.history),
currentQuestion: session.currentQuestion ? JSON.stringify(session.currentQuestion) : null,
@@ -300,7 +305,7 @@ function unpersistMissionSession(sessionId: string): void {
}
function buildMissionInterviewSessionFromRow(row: AiSessionRow): MissionInterviewSession {
const payload = safeParseJson<{ ip?: string; missionId?: string; missionTitle?: string }>(
const payload = safeParseJson<{ ip?: string; missionId?: string; missionTitle?: string; modelProvider?: string; modelId?: string }>(
row.inputPayload,
{},
{ throwOnError: true, fieldName: "inputPayload" },
@@ -338,6 +343,8 @@ function buildMissionInterviewSessionFromRow(row: AiSessionRow): MissionIntervie
thinkingOutput: row.thinkingOutput,
lastGeneratedThinking: row.thinkingOutput || "",
error: row.error ?? undefined,
modelProvider: payload.modelProvider,
modelId: payload.modelId,
createdAt,
updatedAt,
agent: undefined,
@@ -749,6 +756,12 @@ async function createMissionInterviewAgent(
cwd: rootDir,
systemPrompt: effectivePrompt,
tools: "readonly",
...(session.modelProvider && session.modelId
? {
defaultProvider: session.modelProvider,
defaultModelId: session.modelId,
}
: {}),
onThinking: (delta: string) => {
session.thinkingOutput += delta;
persistMissionThinking(session.id, session.thinkingOutput);
@@ -963,6 +976,8 @@ export async function createMissionInterviewSession(
missionTitle: string,
rootDir: string,
promptOverrides?: PromptOverrideMap,
modelProvider?: string,
modelId?: string,
): Promise<string> {
if (!checkRateLimit(ip)) {
const resetTime = getRateLimitResetTime(ip);
@@ -982,6 +997,8 @@ export async function createMissionInterviewSession(
history: [],
thinkingOutput: "",
lastGeneratedThinking: "",
modelProvider,
modelId,
createdAt: new Date(),
updatedAt: new Date(),
};

View File

@@ -354,13 +354,13 @@ export function createMissionRouter(
/**
* POST /api/missions/interview/start
* Start a mission interview session with AI agent streaming.
* Body: { missionTitle: string }
* Body: { missionTitle: string, modelProvider?: string, modelId?: string }
* Returns: { sessionId: string }
*/
router.post(
"/interview/start",
catchTypedHandler(async (req, res) => {
const { missionTitle } = req.body;
const { missionTitle, modelProvider, modelId } = req.body;
if (!missionTitle || typeof missionTitle !== "string" || !missionTitle.trim()) {
throw badRequest("missionTitle is required and must be a non-empty string");
@@ -370,6 +370,19 @@ export function createMissionRouter(
throw badRequest("missionTitle must be 500 characters or less");
}
// Validate model parameters - if one is provided, both must be provided
if (modelProvider !== undefined && typeof modelProvider !== "string") {
throw badRequest("modelProvider must be a string when provided");
}
if (modelId !== undefined && typeof modelId !== "string") {
throw badRequest("modelId must be a string when provided");
}
if ((modelProvider && !modelId) || (!modelProvider && modelId)) {
throw badRequest("Both modelProvider and modelId must be provided together, or neither should be provided");
}
try {
const ip = req.ip || req.socket.remoteAddress || "unknown";
const scopedStore = await getScopedStoreForRequest(req);
@@ -386,6 +399,8 @@ export function createMissionRouter(
missionTitle.trim(),
rootDir,
settings.promptOverrides,
modelProvider,
modelId,
);
res.status(201).json({ sessionId });
} catch (err: any) {