feat(FN-4620): complete Step 4 — add milestone/slice interview board tools

Fusion-Task-Id: FN-4620
Fusion-Task-Lineage: d3d05e54-e137-46ee-bb5d-d71aa03e5a48
This commit is contained in:
Fusion
2026-05-15 11:32:40 -07:00
committed by gsxdsm
parent 0e44bd81c7
commit 808103b5ad
2 changed files with 52 additions and 22 deletions

View File

@@ -15,7 +15,7 @@
* - Unified session type for both milestone and slice interviews
*/
import type { PlanningQuestion, Milestone, Slice, MissionStore, InterviewState, SlicePlanState } from "@fusion/core";
import type { PlanningQuestion, Milestone, Slice, MissionStore, InterviewState, SlicePlanState, TaskStore } from "@fusion/core";
import { randomUUID } from "node:crypto";
import { EventEmitter } from "node:events";
import type { AiSessionStore, AiSessionRow } from "./ai-session-store.js";
@@ -96,6 +96,7 @@ function parseTargetInterviewResponseImpl(text: string): TargetInterviewResponse
export { parseTargetInterviewResponseImpl as parseTargetInterviewResponse };
import { createFnAgent as engineCreateFnAgent } from "@fusion/engine";
import { createPlanningBoardTools } from "./planning-board-tools.js";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AgentResult = any;
@@ -166,6 +167,11 @@ A milestone represents a major phase or deliverable within a larger mission. Eac
- Milestone: "verification" field — how to confirm this phase is complete
- Slice: "verification" field — how to confirm this work unit is done
## Board tools
- fn_task_list — list active tasks
- fn_task_get — read a task's full details and PROMPT.md
Use these to avoid duplicating an existing in-flight plan and to anchor your questions against current backlog context.
## Response Format
Always respond with valid JSON in one of these formats:
@@ -210,6 +216,11 @@ A slice represents a focused work unit within a milestone that can be activated
- Slice: "verification" field — how to confirm this work unit is done
- Feature: "acceptanceCriteria" field — how to verify this specific deliverable
## Board tools
- fn_task_list — list active tasks
- fn_task_get — read a task's full details and PROMPT.md
Use these to avoid duplicating an existing in-flight plan and to anchor your questions against current backlog context.
## Response Format
Always respond with valid JSON in one of these formats:
@@ -717,6 +728,7 @@ function getSystemPrompt(targetType: TargetType): string {
async function createTargetInterviewAgent(
session: TargetInterviewSession,
rootDir: string,
store: TaskStore,
): Promise<AgentResult> {
await ensureEngineReady();
@@ -724,6 +736,7 @@ async function createTargetInterviewAgent(
cwd: rootDir,
systemPrompt: getSystemPrompt(session.targetType),
tools: "readonly",
customTools: [...createPlanningBoardTools(store)],
onThinking: (delta: string) => {
session.thinkingOutput += delta;
persistThinking(session.id, session.thinkingOutput);
@@ -771,6 +784,7 @@ function formatInterviewHistory(
async function ensureInterviewAgent(
session: TargetInterviewSession,
rootDir: string | undefined,
store: TaskStore | undefined,
historyForReplay: Array<{ question: PlanningQuestion; response: unknown }>,
): Promise<void> {
if (session.agent) {
@@ -783,7 +797,13 @@ async function ensureInterviewAgent(
);
}
session.agent = await createTargetInterviewAgent(session, rootDir);
if (!store) {
throw new TargetInvalidSessionStateError(
"AI agent not available for this session and cannot be resumed without task store context",
);
}
session.agent = await createTargetInterviewAgent(session, rootDir, store);
if (historyForReplay.length === 0) {
return;
@@ -820,9 +840,9 @@ async function ensureInterviewAgent(
/**
* Initialize the AI agent for a session and start the first turn.
*/
async function initializeAgent(session: TargetInterviewSession, rootDir: string): Promise<void> {
async function initializeAgent(session: TargetInterviewSession, rootDir: string, store: TaskStore): Promise<void> {
try {
session.agent = await createTargetInterviewAgent(session, rootDir);
session.agent = await createTargetInterviewAgent(session, rootDir, store);
session.updatedAt = new Date();
// Send initial message to get first question
@@ -1002,7 +1022,8 @@ export async function createTargetInterviewSession(
targetId: string,
targetTitle: string,
missionContext: string | undefined,
rootDir: string
rootDir: string,
store: TaskStore,
): Promise<string> {
if (!checkRateLimit(ip)) {
const resetTime = getRateLimitResetTime(ip);
@@ -1032,7 +1053,7 @@ export async function createTargetInterviewSession(
persistSession(session, "generating");
// Initialize AI agent in background
initializeAgent(session, rootDir).catch((err) => {
initializeAgent(session, rootDir, store).catch((err) => {
diagnostics.errorFromException("Failed to initialize agent for session", err, { sessionId, operation: "initialize-agent" });
persistSession(session, "error", err.message || "Failed to initialize AI agent");
milestoneSliceInterviewStreamManager.broadcast(sessionId, {
@@ -1051,6 +1072,7 @@ export async function submitTargetInterviewResponse(
sessionId: string,
responses: Record<string, unknown>,
rootDir?: string,
store?: TaskStore,
): Promise<TargetInterviewResponse> {
const session = getTargetInterviewSession(sessionId);
if (!session) {
@@ -1072,7 +1094,7 @@ export async function submitTargetInterviewResponse(
if (!session.agent) {
const replayHistory = session.history.slice(0, -1);
await ensureInterviewAgent(session, rootDir, replayHistory);
await ensureInterviewAgent(session, rootDir, store, replayHistory);
}
const message = formatResponseForAgent(session.currentQuestion, responses);
@@ -1099,7 +1121,7 @@ export async function submitTargetInterviewResponse(
/**
* Retry a failed interview session.
*/
export async function retryTargetInterviewSession(sessionId: string, rootDir: string): Promise<void> {
export async function retryTargetInterviewSession(sessionId: string, rootDir: string, store?: TaskStore): Promise<void> {
const session = getTargetInterviewSession(sessionId);
if (!session) {
throw new TargetSessionNotFoundError(`Interview session ${sessionId} not found or expired`);
@@ -1126,7 +1148,7 @@ export async function retryTargetInterviewSession(sessionId: string, rootDir: st
persistSession(session, "generating");
if (session.history.length === 0) {
await ensureInterviewAgent(session, rootDir, []);
await ensureInterviewAgent(session, rootDir, store, []);
await continueAgentConversation(
session,
`I want to refine the scope for this ${session.targetType}: "${session.targetTitle}".` +
@@ -1139,7 +1161,7 @@ export async function retryTargetInterviewSession(sessionId: string, rootDir: st
const replayHistory = session.history.slice(0, -1);
const lastEntry = session.history[session.history.length - 1];
await ensureInterviewAgent(session, rootDir, replayHistory);
await ensureInterviewAgent(session, rootDir, store, replayHistory);
const replayMessage = formatResponseForAgent(
lastEntry.question,
coerceResponseRecord(lastEntry.question, lastEntry.response),