FN-6622: wire dashboard interviews to session skills
Dashboard interview lanes now request the same role-fallback and enabled plugin skills as agent-acting sessions. - Add skillSelection to agent onboarding and milestone/slice interview agent creation. - Thread the plugin runner through mission interview and onboarding routes. - Cover enabled and disabled plugin skill selection behavior in dashboard tests. - Document the newly covered dashboard interview lanes and add a minor changeset. Files changed: .../fn-6622-session-skill-interview-lanes.md | 5 ++ docs/agents.md | 2 +- .../src/__tests__/agent-onboarding.test.ts | 68 ++++++++++++++++++++++ .../__tests__/milestone-slice-interview.test.ts | 66 +++++++++++++++++++++ packages/dashboard/src/agent-onboarding.ts | 10 +++- .../dashboard/src/milestone-slice-interview.ts | 39 ++++++++++--- packages/dashboard/src/mission-routes.ts | 10 ++-- ...gister-agent-import-export-generation-routes.ts | 3 +- 8 files changed, 187 insertions(+), 16 deletions(-) Fusion-Task-Id: FN-6622 Fusion-Task-Lineage: 5e3dded1-1866-4b76-8302-253e5b3867fa
This commit is contained in:
5
.changeset/fn-6622-session-skill-interview-lanes.md
Normal file
5
.changeset/fn-6622-session-skill-interview-lanes.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Load selected Fusion and enabled plugin skills in milestone/slice interview and agent-onboarding dashboard sessions.
|
||||||
@@ -24,7 +24,7 @@ fn chat <agent-id> [message…] [--once] [--non-interactive] [--poll-ms <n>]
|
|||||||
- Each message is stored as a `user-to-agent` MessageStore message from `cli` with `metadata.wakeRecipient=true`.
|
- Each message is stored as a `user-to-agent` MessageStore message from `cli` with `metadata.wakeRecipient=true`.
|
||||||
- Agent replies are polled from your inbox and printed as they arrive.
|
- Agent replies are polled from your inbox and printed as they arrive.
|
||||||
- Dashboard-created agent chat sessions request the target agent's declared `metadata.skills` plus enabled plugin-contributed skills, so skills such as `ce-debug` are available in chat when the contributing plugin is enabled. Model-only QuickChat sessions request enabled plugin skills, and room responder sessions request the responder agent's skills.
|
- Dashboard-created agent chat sessions request the target agent's declared `metadata.skills` plus enabled plugin-contributed skills, so skills such as `ce-debug` are available in chat when the contributing plugin is enabled. Model-only QuickChat sessions request enabled plugin skills, and room responder sessions request the responder agent's skills.
|
||||||
- Agent-acting session lanes share the same skill-injection contract as executor sessions: executor, merger, triage, reviewer, heartbeat, step-session, dashboard chat/room responders, CLI agent execution, planning, mission interview, workflow design, memory dreams/insight extraction, and scheduled cron automation all request agent/fallback skills plus enabled plugin-contributed skills when a plugin runner is available. Utility-only lanes that only summarize/extract/generate JSON (title/PR summaries, memory compaction, subtask breakdown, text refinement, agent generation, PR metadata generation, evaluator/research synthesis, and similar one-shot helpers) intentionally stay exempt to avoid loading skills where no agent-style tool loop can use them.
|
- Agent-acting session lanes share the same skill-injection contract as executor sessions: executor, merger, triage, reviewer, heartbeat, step-session, dashboard chat/room responders, CLI agent execution, planning, mission interview, milestone/slice interview, agent-onboarding interview, workflow design, memory dreams/insight extraction, and scheduled cron automation all request agent/fallback skills plus enabled plugin-contributed skills when a plugin runner is available. Utility-only lanes that only summarize/extract/generate JSON (title/PR summaries, memory compaction, subtask breakdown, text refinement, agent generation, PR metadata generation, evaluator/research synthesis, and similar one-shot helpers) intentionally stay exempt to avoid loading skills where no agent-style tool loop can use them.
|
||||||
- In dashboard model-loop chat (main chat, QuickChat, and room responders), typing `/skill:{name}` requests that skill for the current AI session and strips the slash token from the prompt sent to the model. The requested skill is still subject to the normal enabled/disabled execution-skill filters; CLI-agent-backed PTY chat keeps raw terminal input semantics and does not interpret this command.
|
- In dashboard model-loop chat (main chat, QuickChat, and room responders), typing `/skill:{name}` requests that skill for the current AI session and strips the slash token from the prompt sent to the model. The requested skill is still subject to the normal enabled/disabled execution-skill filters; CLI-agent-backed PTY chat keeps raw terminal input semantics and does not interpret this command.
|
||||||
|
|
||||||
### Flags
|
### Flags
|
||||||
|
|||||||
@@ -6,6 +6,21 @@ const { mockCreateFnAgent } = vi.hoisted(() => ({
|
|||||||
|
|
||||||
vi.mock("@fusion/engine", () => ({
|
vi.mock("@fusion/engine", () => ({
|
||||||
listCliAdapterDescriptors: () => [],
|
listCliAdapterDescriptors: () => [],
|
||||||
|
buildSessionSkillContextSync: (_agent: unknown, sessionPurpose: string, projectRootDir: string, pluginRunner?: { getPluginSkills?: () => Array<{ pluginId: string; skill: { name: string; enabled?: boolean } }> }) => {
|
||||||
|
const requestedSkillNames = ["fusion"];
|
||||||
|
for (const contribution of pluginRunner?.getPluginSkills?.() ?? []) {
|
||||||
|
const name = contribution.skill.name.trim();
|
||||||
|
if (contribution.skill.enabled === false || name.length === 0 || requestedSkillNames.includes(name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
requestedSkillNames.push(name);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
skillSelectionContext: { projectRootDir, requestedSkillNames, sessionPurpose },
|
||||||
|
resolvedSkillNames: requestedSkillNames,
|
||||||
|
skillSource: "role-fallback" as const,
|
||||||
|
};
|
||||||
|
},
|
||||||
createFnAgent: mockCreateFnAgent,
|
createFnAgent: mockCreateFnAgent,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -47,6 +62,12 @@ async function waitFor(check: () => boolean, timeoutMs = 2000): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createSkillPluginRunner(skills: Array<{ name: string; enabled?: boolean }>) {
|
||||||
|
return {
|
||||||
|
getPluginSkills: () => skills.map((skill) => ({ pluginId: "fusion-plugin-compound-engineering", skill })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
describe("agent-onboarding", () => {
|
describe("agent-onboarding", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -246,6 +267,53 @@ describe("agent-onboarding", () => {
|
|||||||
expect(prompt).toContain("messageResponseMode: immediate");
|
expect(prompt).toContain("messageResponseMode: immediate");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("requests role-fallback and enabled plugin skills for model-only onboarding agents", async () => {
|
||||||
|
mockCreateFnAgent.mockResolvedValueOnce(
|
||||||
|
createMockAgent([
|
||||||
|
JSON.stringify({
|
||||||
|
type: "question",
|
||||||
|
data: { id: "goal", type: "text", question: "What is the primary goal?" },
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
await startAgentOnboardingSession(
|
||||||
|
"127.0.0.1",
|
||||||
|
{ intent: "skills", existingAgents: [], templates: [] },
|
||||||
|
process.cwd(),
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
createSkillPluginRunner([
|
||||||
|
{ name: "ce-debug" },
|
||||||
|
{ name: "disabled-skill", enabled: false },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const options = mockCreateFnAgent.mock.calls.at(-1)?.[0] as { skillSelection?: { requestedSkillNames?: string[] } };
|
||||||
|
expect(options.skillSelection?.requestedSkillNames).toEqual(["fusion", "ce-debug"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requests role-fallback skills when onboarding plugin runner is unavailable", async () => {
|
||||||
|
mockCreateFnAgent.mockResolvedValueOnce(
|
||||||
|
createMockAgent([
|
||||||
|
JSON.stringify({
|
||||||
|
type: "question",
|
||||||
|
data: { id: "goal", type: "text", question: "What is the primary goal?" },
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
await startAgentOnboardingSession(
|
||||||
|
"127.0.0.1",
|
||||||
|
{ intent: "skills", existingAgents: [], templates: [] },
|
||||||
|
process.cwd(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const options = mockCreateFnAgent.mock.calls.at(-1)?.[0] as { skillSelection?: { requestedSkillNames?: string[] } };
|
||||||
|
expect(options.skillSelection?.requestedSkillNames).toEqual(["fusion"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("progresses through start -> question -> response -> final summary", async () => {
|
it("progresses through start -> question -> response -> final summary", async () => {
|
||||||
mockCreateFnAgent.mockResolvedValueOnce(
|
mockCreateFnAgent.mockResolvedValueOnce(
|
||||||
createMockAgent([
|
createMockAgent([
|
||||||
|
|||||||
@@ -8,6 +8,21 @@ const { mockCreateFnAgent } = vi.hoisted(() => ({
|
|||||||
|
|
||||||
vi.mock("@fusion/engine", () => ({
|
vi.mock("@fusion/engine", () => ({
|
||||||
listCliAdapterDescriptors: () => [],
|
listCliAdapterDescriptors: () => [],
|
||||||
|
buildSessionSkillContextSync: (_agent: unknown, sessionPurpose: string, projectRootDir: string, pluginRunner?: { getPluginSkills?: () => Array<{ pluginId: string; skill: { name: string; enabled?: boolean } }> }) => {
|
||||||
|
const requestedSkillNames = ["fusion"];
|
||||||
|
for (const contribution of pluginRunner?.getPluginSkills?.() ?? []) {
|
||||||
|
const name = contribution.skill.name.trim();
|
||||||
|
if (contribution.skill.enabled === false || name.length === 0 || requestedSkillNames.includes(name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
requestedSkillNames.push(name);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
skillSelectionContext: { projectRootDir, requestedSkillNames, sessionPurpose },
|
||||||
|
resolvedSkillNames: requestedSkillNames,
|
||||||
|
skillSource: "role-fallback" as const,
|
||||||
|
};
|
||||||
|
},
|
||||||
createFnAgent: mockCreateFnAgent,
|
createFnAgent: mockCreateFnAgent,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -140,6 +155,23 @@ async function waitForCurrentQuestion(sessionId: string): Promise<void> {
|
|||||||
throw new Error("Timed out waiting for currentQuestion");
|
throw new Error("Timed out waiting for currentQuestion");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function waitForCreateFnAgentOptions(): Promise<{ skillSelection?: { requestedSkillNames?: string[] } }> {
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
const options = mockCreateFnAgent.mock.calls.at(-1)?.[0];
|
||||||
|
if (options) {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||||
|
}
|
||||||
|
throw new Error("Timed out waiting for createFnAgent options");
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSkillPluginRunner(skills: Array<{ name: string; enabled?: boolean }>) {
|
||||||
|
return {
|
||||||
|
getPluginSkills: () => skills.map((skill) => ({ pluginId: "fusion-plugin-compound-engineering", skill })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
class MockAiSessionStore extends EventEmitter {
|
class MockAiSessionStore extends EventEmitter {
|
||||||
rows = new Map<string, AiSessionRow>();
|
rows = new Map<string, AiSessionRow>();
|
||||||
|
|
||||||
@@ -235,6 +267,40 @@ describe("milestone-slice-interview module", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("session lifecycle", () => {
|
describe("session lifecycle", () => {
|
||||||
|
it("requests role-fallback and enabled plugin skills for model-only milestone/slice interview agents", async () => {
|
||||||
|
await createTargetInterviewSession(
|
||||||
|
"127.0.0.1",
|
||||||
|
"milestone",
|
||||||
|
"ms-skills",
|
||||||
|
"Skillful Milestone",
|
||||||
|
undefined,
|
||||||
|
"/tmp/project",
|
||||||
|
MOCK_TASK_STORE,
|
||||||
|
createSkillPluginRunner([
|
||||||
|
{ name: "ce-debug" },
|
||||||
|
{ name: "disabled-skill", enabled: false },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const options = await waitForCreateFnAgentOptions();
|
||||||
|
expect(options.skillSelection?.requestedSkillNames).toEqual(["fusion", "ce-debug"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requests role-fallback skills when milestone/slice plugin runner is unavailable", async () => {
|
||||||
|
await createTargetInterviewSession(
|
||||||
|
"127.0.0.1",
|
||||||
|
"slice",
|
||||||
|
"sl-skills",
|
||||||
|
"Fallback Slice",
|
||||||
|
undefined,
|
||||||
|
"/tmp/project",
|
||||||
|
MOCK_TASK_STORE,
|
||||||
|
);
|
||||||
|
|
||||||
|
const options = await waitForCreateFnAgentOptions();
|
||||||
|
expect(options.skillSelection?.requestedSkillNames).toEqual(["fusion"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("creates, retrieves, and cleans up a milestone session", async () => {
|
it("creates, retrieves, and cleans up a milestone session", async () => {
|
||||||
const sessionId = await createTargetInterviewSession(
|
const sessionId = await createTargetInterviewSession(
|
||||||
"127.0.0.1",
|
"127.0.0.1",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|||||||
import { EventEmitter } from "node:events";
|
import { EventEmitter } from "node:events";
|
||||||
import type { AgentCapability, PlanningQuestion } from "@fusion/core";
|
import type { AgentCapability, PlanningQuestion } from "@fusion/core";
|
||||||
import { resolvePrompt, type PromptOverrideMap } from "@fusion/core";
|
import { resolvePrompt, type PromptOverrideMap } from "@fusion/core";
|
||||||
import { createFnAgent as engineCreateFnAgent } from "@fusion/engine";
|
import { buildSessionSkillContextSync, createFnAgent as engineCreateFnAgent } from "@fusion/engine";
|
||||||
import { SessionEventBuffer, type SessionBufferedEvent } from "./sse-buffer.js";
|
import { SessionEventBuffer, type SessionBufferedEvent } from "./sse-buffer.js";
|
||||||
|
|
||||||
export interface AgentOnboardingSummary {
|
export interface AgentOnboardingSummary {
|
||||||
@@ -61,6 +61,7 @@ export type AgentOnboardingStreamEvent =
|
|||||||
export type AgentOnboardingStreamCallback = (event: AgentOnboardingStreamEvent, eventId?: number) => void;
|
export type AgentOnboardingStreamCallback = (event: AgentOnboardingStreamEvent, eventId?: number) => void;
|
||||||
|
|
||||||
const createFnAgent: typeof engineCreateFnAgent = engineCreateFnAgent;
|
const createFnAgent: typeof engineCreateFnAgent = engineCreateFnAgent;
|
||||||
|
type SkillSelectionPluginRunner = Parameters<typeof buildSessionSkillContextSync>[3];
|
||||||
const SESSION_TTL_MS = 30 * 60 * 1000;
|
const SESSION_TTL_MS = 30 * 60 * 1000;
|
||||||
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
|
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
|
||||||
const GENERATION_TIMEOUT_MS = 120_000;
|
const GENERATION_TIMEOUT_MS = 120_000;
|
||||||
@@ -281,6 +282,7 @@ export async function startAgentOnboardingSession(
|
|||||||
modelProvider?: string,
|
modelProvider?: string,
|
||||||
modelId?: string,
|
modelId?: string,
|
||||||
promptOverrides?: PromptOverrideMap,
|
promptOverrides?: PromptOverrideMap,
|
||||||
|
pluginRunner?: SkillSelectionPluginRunner,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const id = randomUUID();
|
const id = randomUUID();
|
||||||
const mode: OnboardingMode = initialContext.mode ?? "create";
|
const mode: OnboardingMode = initialContext.mode ?? "create";
|
||||||
@@ -303,11 +305,17 @@ export async function startAgentOnboardingSession(
|
|||||||
sessions.set(id, session);
|
sessions.set(id, session);
|
||||||
|
|
||||||
const systemPrompt = resolvePrompt("agent-onboarding-system", promptOverrides) || AGENT_ONBOARDING_SYSTEM_PROMPT;
|
const systemPrompt = resolvePrompt("agent-onboarding-system", promptOverrides) || AGENT_ONBOARDING_SYSTEM_PROMPT;
|
||||||
|
const skillContext = buildSessionSkillContextSync(null, "executor", rootDir, pluginRunner);
|
||||||
session.agent = await createFnAgent({
|
session.agent = await createFnAgent({
|
||||||
cwd: rootDir,
|
cwd: rootDir,
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
tools: "readonly",
|
tools: "readonly",
|
||||||
...(modelProvider && modelId ? { defaultProvider: modelProvider, defaultModelId: modelId } : {}),
|
...(modelProvider && modelId ? { defaultProvider: modelProvider, defaultModelId: modelId } : {}),
|
||||||
|
/*
|
||||||
|
FNXC:InterviewSkills 2026-06-17-21:53:
|
||||||
|
Agent onboarding is a model-only dashboard interview lane, so it must request executor role-fallback skills plus enabled plugin skills such as ce-debug like other agent-acting sessions.
|
||||||
|
*/
|
||||||
|
...(skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}),
|
||||||
onThinking: (delta: string) => {
|
onThinking: (delta: string) => {
|
||||||
session.thinkingOutput += delta;
|
session.thinkingOutput += delta;
|
||||||
agentOnboardingStreamManager.broadcast(session.id, { type: "thinking", data: delta });
|
agentOnboardingStreamManager.broadcast(session.id, { type: "thinking", data: delta });
|
||||||
|
|||||||
@@ -96,11 +96,12 @@ 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 };
|
||||||
|
|
||||||
import { createFnAgent as engineCreateFnAgent } from "@fusion/engine";
|
import { buildSessionSkillContextSync, createFnAgent as engineCreateFnAgent } from "@fusion/engine";
|
||||||
import { createPlanningBoardTools } from "./planning-board-tools.js";
|
import { createPlanningBoardTools } from "./planning-board-tools.js";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
type AgentResult = any;
|
type AgentResult = any;
|
||||||
|
type SkillSelectionPluginRunner = Parameters<typeof buildSessionSkillContextSync>[3];
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const createFnAgent: any = engineCreateFnAgent;
|
const createFnAgent: any = engineCreateFnAgent;
|
||||||
|
|
||||||
@@ -730,14 +731,21 @@ async function createTargetInterviewAgent(
|
|||||||
session: TargetInterviewSession,
|
session: TargetInterviewSession,
|
||||||
rootDir: string,
|
rootDir: string,
|
||||||
store: TaskStore,
|
store: TaskStore,
|
||||||
|
pluginRunner?: SkillSelectionPluginRunner,
|
||||||
): Promise<AgentResult> {
|
): Promise<AgentResult> {
|
||||||
await ensureEngineReady();
|
await ensureEngineReady();
|
||||||
|
const skillContext = buildSessionSkillContextSync(null, "executor", rootDir, pluginRunner);
|
||||||
|
|
||||||
return createFnAgent({
|
return createFnAgent({
|
||||||
cwd: rootDir,
|
cwd: rootDir,
|
||||||
systemPrompt: getSystemPrompt(session.targetType),
|
systemPrompt: getSystemPrompt(session.targetType),
|
||||||
tools: "readonly",
|
tools: "readonly",
|
||||||
customTools: [...createPlanningBoardTools(store)],
|
customTools: [...createPlanningBoardTools(store)],
|
||||||
|
/*
|
||||||
|
FNXC:InterviewSkills 2026-06-17-21:42:
|
||||||
|
Milestone and slice interview agents are model-only tool-loop sessions, so they must request executor role-fallback skills plus enabled plugin skills such as ce-debug instead of creating skill-less dashboard sessions.
|
||||||
|
*/
|
||||||
|
...(skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}),
|
||||||
onThinking: (delta: string) => {
|
onThinking: (delta: string) => {
|
||||||
session.thinkingOutput += delta;
|
session.thinkingOutput += delta;
|
||||||
persistThinking(session.id, session.thinkingOutput);
|
persistThinking(session.id, session.thinkingOutput);
|
||||||
@@ -787,6 +795,7 @@ async function ensureInterviewAgent(
|
|||||||
rootDir: string | undefined,
|
rootDir: string | undefined,
|
||||||
store: TaskStore | undefined,
|
store: TaskStore | undefined,
|
||||||
historyForReplay: Array<{ question: PlanningQuestion; response: unknown }>,
|
historyForReplay: Array<{ question: PlanningQuestion; response: unknown }>,
|
||||||
|
pluginRunner?: SkillSelectionPluginRunner,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (session.agent) {
|
if (session.agent) {
|
||||||
return;
|
return;
|
||||||
@@ -804,7 +813,7 @@ async function ensureInterviewAgent(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
session.agent = await createTargetInterviewAgent(session, rootDir, store);
|
session.agent = await createTargetInterviewAgent(session, rootDir, store, pluginRunner);
|
||||||
|
|
||||||
if (historyForReplay.length === 0) {
|
if (historyForReplay.length === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -841,9 +850,14 @@ async function ensureInterviewAgent(
|
|||||||
/**
|
/**
|
||||||
* Initialize the AI agent for a session and start the first turn.
|
* Initialize the AI agent for a session and start the first turn.
|
||||||
*/
|
*/
|
||||||
async function initializeAgent(session: TargetInterviewSession, rootDir: string, store: TaskStore): Promise<void> {
|
async function initializeAgent(
|
||||||
|
session: TargetInterviewSession,
|
||||||
|
rootDir: string,
|
||||||
|
store: TaskStore,
|
||||||
|
pluginRunner?: SkillSelectionPluginRunner,
|
||||||
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
session.agent = await createTargetInterviewAgent(session, rootDir, store);
|
session.agent = await createTargetInterviewAgent(session, rootDir, store, pluginRunner);
|
||||||
session.updatedAt = new Date();
|
session.updatedAt = new Date();
|
||||||
|
|
||||||
// Send initial message to get first question
|
// Send initial message to get first question
|
||||||
@@ -1025,6 +1039,7 @@ export async function createTargetInterviewSession(
|
|||||||
missionContext: string | undefined,
|
missionContext: string | undefined,
|
||||||
rootDir: string,
|
rootDir: string,
|
||||||
store: TaskStore,
|
store: TaskStore,
|
||||||
|
pluginRunner?: SkillSelectionPluginRunner,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (!checkRateLimit(ip)) {
|
if (!checkRateLimit(ip)) {
|
||||||
const resetTime = getRateLimitResetTime(ip);
|
const resetTime = getRateLimitResetTime(ip);
|
||||||
@@ -1054,7 +1069,7 @@ export async function createTargetInterviewSession(
|
|||||||
persistSession(session, "generating");
|
persistSession(session, "generating");
|
||||||
|
|
||||||
// Initialize AI agent in background
|
// Initialize AI agent in background
|
||||||
initializeAgent(session, rootDir, store).catch((err) => {
|
initializeAgent(session, rootDir, store, pluginRunner).catch((err) => {
|
||||||
diagnostics.errorFromException("Failed to initialize agent for session", err, { sessionId, operation: "initialize-agent" });
|
diagnostics.errorFromException("Failed to initialize agent for session", err, { sessionId, operation: "initialize-agent" });
|
||||||
persistSession(session, "error", err.message || "Failed to initialize AI agent");
|
persistSession(session, "error", err.message || "Failed to initialize AI agent");
|
||||||
milestoneSliceInterviewStreamManager.broadcast(sessionId, {
|
milestoneSliceInterviewStreamManager.broadcast(sessionId, {
|
||||||
@@ -1074,6 +1089,7 @@ export async function submitTargetInterviewResponse(
|
|||||||
responses: Record<string, unknown>,
|
responses: Record<string, unknown>,
|
||||||
rootDir?: string,
|
rootDir?: string,
|
||||||
store?: TaskStore,
|
store?: TaskStore,
|
||||||
|
pluginRunner?: SkillSelectionPluginRunner,
|
||||||
): Promise<TargetInterviewResponse> {
|
): Promise<TargetInterviewResponse> {
|
||||||
const session = getTargetInterviewSession(sessionId);
|
const session = getTargetInterviewSession(sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
@@ -1095,7 +1111,7 @@ export async function submitTargetInterviewResponse(
|
|||||||
|
|
||||||
if (!session.agent) {
|
if (!session.agent) {
|
||||||
const replayHistory = session.history.slice(0, -1);
|
const replayHistory = session.history.slice(0, -1);
|
||||||
await ensureInterviewAgent(session, rootDir, store, replayHistory);
|
await ensureInterviewAgent(session, rootDir, store, replayHistory, pluginRunner);
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = formatResponseForAgent(session.currentQuestion, responses);
|
const message = formatResponseForAgent(session.currentQuestion, responses);
|
||||||
@@ -1122,7 +1138,12 @@ export async function submitTargetInterviewResponse(
|
|||||||
/**
|
/**
|
||||||
* Retry a failed interview session.
|
* Retry a failed interview session.
|
||||||
*/
|
*/
|
||||||
export async function retryTargetInterviewSession(sessionId: string, rootDir: string, store?: TaskStore): Promise<void> {
|
export async function retryTargetInterviewSession(
|
||||||
|
sessionId: string,
|
||||||
|
rootDir: string,
|
||||||
|
store?: TaskStore,
|
||||||
|
pluginRunner?: SkillSelectionPluginRunner,
|
||||||
|
): Promise<void> {
|
||||||
const session = getTargetInterviewSession(sessionId);
|
const session = getTargetInterviewSession(sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new TargetSessionNotFoundError(`Interview session ${sessionId} not found or expired`);
|
throw new TargetSessionNotFoundError(`Interview session ${sessionId} not found or expired`);
|
||||||
@@ -1149,7 +1170,7 @@ export async function retryTargetInterviewSession(sessionId: string, rootDir: st
|
|||||||
persistSession(session, "generating");
|
persistSession(session, "generating");
|
||||||
|
|
||||||
if (session.history.length === 0) {
|
if (session.history.length === 0) {
|
||||||
await ensureInterviewAgent(session, rootDir, store, []);
|
await ensureInterviewAgent(session, rootDir, store, [], pluginRunner);
|
||||||
await continueAgentConversation(
|
await continueAgentConversation(
|
||||||
session,
|
session,
|
||||||
`I want to refine the scope for this ${session.targetType}: "${session.targetTitle}".` +
|
`I want to refine the scope for this ${session.targetType}: "${session.targetTitle}".` +
|
||||||
@@ -1162,7 +1183,7 @@ export async function retryTargetInterviewSession(sessionId: string, rootDir: st
|
|||||||
const replayHistory = session.history.slice(0, -1);
|
const replayHistory = session.history.slice(0, -1);
|
||||||
const lastEntry = session.history[session.history.length - 1];
|
const lastEntry = session.history[session.history.length - 1];
|
||||||
|
|
||||||
await ensureInterviewAgent(session, rootDir, store, replayHistory);
|
await ensureInterviewAgent(session, rootDir, store, replayHistory, pluginRunner);
|
||||||
const replayMessage = formatResponseForAgent(
|
const replayMessage = formatResponseForAgent(
|
||||||
lastEntry.question,
|
lastEntry.question,
|
||||||
coerceResponseRecord(lastEntry.question, lastEntry.response),
|
coerceResponseRecord(lastEntry.question, lastEntry.response),
|
||||||
|
|||||||
@@ -3294,6 +3294,7 @@ export function createMissionRouter(
|
|||||||
missionContext,
|
missionContext,
|
||||||
rootDir,
|
rootDir,
|
||||||
scopedStore,
|
scopedStore,
|
||||||
|
pluginRunner,
|
||||||
);
|
);
|
||||||
res.status(201).json({ sessionId });
|
res.status(201).json({ sessionId });
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
@@ -3347,7 +3348,7 @@ export function createMissionRouter(
|
|||||||
|
|
||||||
const { store: scopedStore } = await getProjectContext(req);
|
const { store: scopedStore } = await getProjectContext(req);
|
||||||
const rootDir = scopedStore.getRootDir();
|
const rootDir = scopedStore.getRootDir();
|
||||||
const result = await submitTargetInterviewResponse(sessionId, responses, rootDir, scopedStore);
|
const result = await submitTargetInterviewResponse(sessionId, responses, rootDir, scopedStore, pluginRunner);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const errName = err instanceof Error ? err.name : "";
|
const errName = err instanceof Error ? err.name : "";
|
||||||
@@ -3511,7 +3512,7 @@ export function createMissionRouter(
|
|||||||
|
|
||||||
const { store: scopedStore } = await getProjectContext(req);
|
const { store: scopedStore } = await getProjectContext(req);
|
||||||
const rootDir = scopedStore.getRootDir();
|
const rootDir = scopedStore.getRootDir();
|
||||||
await retryTargetInterviewSession(sessionId, rootDir, scopedStore);
|
await retryTargetInterviewSession(sessionId, rootDir, scopedStore, pluginRunner);
|
||||||
res.json({ success: true, sessionId });
|
res.json({ success: true, sessionId });
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const errName = err instanceof Error ? err.name : "";
|
const errName = err instanceof Error ? err.name : "";
|
||||||
@@ -3642,6 +3643,7 @@ export function createMissionRouter(
|
|||||||
missionContext,
|
missionContext,
|
||||||
rootDir,
|
rootDir,
|
||||||
scopedStore,
|
scopedStore,
|
||||||
|
pluginRunner,
|
||||||
);
|
);
|
||||||
res.status(201).json({ sessionId });
|
res.status(201).json({ sessionId });
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
@@ -3695,7 +3697,7 @@ export function createMissionRouter(
|
|||||||
|
|
||||||
const { store: scopedStore } = await getProjectContext(req);
|
const { store: scopedStore } = await getProjectContext(req);
|
||||||
const rootDir = scopedStore.getRootDir();
|
const rootDir = scopedStore.getRootDir();
|
||||||
const result = await submitTargetInterviewResponse(sessionId, responses, rootDir, scopedStore);
|
const result = await submitTargetInterviewResponse(sessionId, responses, rootDir, scopedStore, pluginRunner);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const errName = err instanceof Error ? err.name : "";
|
const errName = err instanceof Error ? err.name : "";
|
||||||
@@ -3859,7 +3861,7 @@ export function createMissionRouter(
|
|||||||
|
|
||||||
const { store: scopedStore } = await getProjectContext(req);
|
const { store: scopedStore } = await getProjectContext(req);
|
||||||
const rootDir = scopedStore.getRootDir();
|
const rootDir = scopedStore.getRootDir();
|
||||||
await retryTargetInterviewSession(sessionId, rootDir, scopedStore);
|
await retryTargetInterviewSession(sessionId, rootDir, scopedStore, pluginRunner);
|
||||||
res.json({ success: true, sessionId });
|
res.json({ success: true, sessionId });
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const errName = err instanceof Error ? err.name : "";
|
const errName = err instanceof Error ? err.name : "";
|
||||||
|
|||||||
@@ -875,7 +875,7 @@ async function persistImportedSkills(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function registerAgentGenerationRoutes(ctx: ApiRoutesContext): void {
|
export function registerAgentGenerationRoutes(ctx: ApiRoutesContext): void {
|
||||||
const { router, getProjectContext, rethrowAsApiError } = ctx;
|
const { router, getProjectContext, rethrowAsApiError, options } = ctx;
|
||||||
const agentGenerationDiagnostics = createSessionDiagnostics("agent-generation");
|
const agentGenerationDiagnostics = createSessionDiagnostics("agent-generation");
|
||||||
|
|
||||||
router.post("/agents/onboarding/start-streaming", async (req, res) => {
|
router.post("/agents/onboarding/start-streaming", async (req, res) => {
|
||||||
@@ -922,6 +922,7 @@ export function registerAgentGenerationRoutes(ctx: ApiRoutesContext): void {
|
|||||||
planningModelProvider,
|
planningModelProvider,
|
||||||
planningModelId,
|
planningModelId,
|
||||||
settings.promptOverrides,
|
settings.promptOverrides,
|
||||||
|
options?.pluginRunner as Parameters<typeof import("@fusion/engine").buildSessionSkillContextSync>[3],
|
||||||
);
|
);
|
||||||
|
|
||||||
res.status(201).json({ sessionId });
|
res.status(201).json({ sessionId });
|
||||||
|
|||||||
Reference in New Issue
Block a user