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:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user