feat(FN-1984): enrich chat and heartbeat prompts with agent context

- Add agent memory support to instruction resolution and introduce/export buildAgentChatPrompt for identity-aware prompt assembly with optional project memory
- Wire ChatManager to AgentStore so chat sessions can enrich system prompts per agent and include recent conversation context in prompt payloads
- Update server chat wiring to provide an AgentStore for prompt enrichment and keep graceful fallback behavior when agent context is unavailable
- Enrich HeartbeatMonitor system prompts with resolved agent instructions/ratings while preserving base-prompt fallback paths
- Expand dashboard and engine tests to cover prompt enrichment, fallback behavior, conversation context assembly, and AgentStore route mocks
This commit is contained in:
Fusion
2026-04-17 02:19:54 -07:00
committed by gsxdsm
parent 7a971a3513
commit 07ddb1ca1f
11 changed files with 431 additions and 57 deletions

View File

@@ -13,6 +13,8 @@
*/
import type {
Agent,
AgentStore,
ChatStore,
ChatSession,
ChatSessionCreateInput,
@@ -27,10 +29,12 @@ import { SessionEventBuffer } from "./sse-buffer.js";
type AgentResult = any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let createKbAgent: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let buildAgentChatPromptFn: any;
// Initialize the import (this runs in actual server, mocked in tests)
async function initEngine() {
if (!createKbAgent) {
if (!createKbAgent || !buildAgentChatPromptFn) {
try {
// Use dynamic import with variable to prevent static analysis
const engineModule = "@fusion/engine";
@@ -38,11 +42,17 @@ async function initEngine() {
if (!createKbAgent) {
createKbAgent = engine.createKbAgent;
}
if (!buildAgentChatPromptFn) {
buildAgentChatPromptFn = engine.buildAgentChatPrompt;
}
} catch {
// Allow failure in test environments - agent functionality will be stubbed
if (!createKbAgent) {
createKbAgent = undefined;
}
if (!buildAgentChatPromptFn) {
buildAgentChatPromptFn = undefined;
}
}
}
}
@@ -255,9 +265,12 @@ export function getRateLimitResetTime(ip: string): Date | null {
* Creates sessions, sends messages, and streams AI responses via SSE.
*/
export class ChatManager {
private agentStoreReady?: Promise<void>;
constructor(
private chatStore: ChatStore,
private rootDir: string,
private agentStore?: AgentStore,
) {}
/**
@@ -357,10 +370,60 @@ export class ChatManager {
throw new Error("AI agent not available");
}
let systemPrompt = CHAT_SYSTEM_PROMPT;
let agent: Agent | null = null;
if (this.agentStore && session.agentId) {
try {
this.agentStoreReady ??= this.agentStore.init();
await this.agentStoreReady;
agent = await this.agentStore.getAgent(session.agentId);
} catch (agentLoadError) {
const message = agentLoadError instanceof Error ? agentLoadError.message : String(agentLoadError);
console.warn(`[chat] Failed to load agent context for ${session.agentId}: ${message}`);
}
}
if (agent && buildAgentChatPromptFn) {
try {
systemPrompt = await buildAgentChatPromptFn({
agent,
rootDir: this.rootDir,
agentStore: this.agentStore,
basePrompt: CHAT_SYSTEM_PROMPT,
includeProjectMemory: true,
});
} catch (promptBuildError) {
const message = promptBuildError instanceof Error ? promptBuildError.message : String(promptBuildError);
console.warn(`[chat] Failed to build enriched system prompt for ${agent.id}: ${message}`);
}
}
const allMessages = this.chatStore.getMessages(sessionId, { limit: 10000 }) ?? [];
const previousMessages = allMessages.slice(-51, -1);
const conversationMessages = previousMessages.filter(
(message) => message.role === "user" || message.role === "assistant",
);
const promptContent = conversationMessages.length > 0
? [
"## Previous Conversation",
"",
...conversationMessages.map((message) => {
const speaker = message.role === "user" ? "User" : "Assistant";
return `[${speaker}]: ${message.content}`;
}),
"",
"## Current Message",
"",
content,
].join("\n")
: content;
// Create AI agent session
agentResult = await createKbAgent({
cwd: this.rootDir,
systemPrompt: CHAT_SYSTEM_PROMPT,
systemPrompt,
tools: "readonly",
...(effectiveModelProvider && effectiveModelId
? {
@@ -385,7 +448,7 @@ export class ChatManager {
});
// Send user message and get response
await agentResult.session.prompt(content);
await agentResult.session.prompt(promptContent);
// Extract response text from agent state
let responseText = "";
@@ -452,10 +515,19 @@ export function __setCreateKbAgent(mock: typeof createKbAgent): void {
createKbAgent = mock;
}
/**
* Inject a mock buildAgentChatPrompt function. Used for testing only.
*/
export function __setBuildAgentChatPrompt(mock: typeof buildAgentChatPromptFn): void {
buildAgentChatPromptFn = mock;
}
/**
* Reset all chat state. Used for testing only.
*/
export function __resetChatState(): void {
chatStreamManager.reset();
rateLimits.clear();
engineReady = undefined;
buildAgentChatPromptFn = undefined;
}