Refactor conversation state to use dynamic system prompts

Move system prompt management from ChatView into ConversationStateManager,
ensuring the system prompt stays synchronized with the current agent mode.
Replace hardcoded default prompts with a shared constant and add setSystemPrompt
to support live updates when switching modes. Clean up minor formatting issues.
This commit is contained in:
2026-05-21 09:09:55 +02:00
parent 96f201bf3f
commit 68ac64cc02
4 changed files with 54 additions and 89 deletions
+10 -8
View File
@@ -25,10 +25,7 @@ function filterToolsByName(tools: OllamaTool[], allowed: Set<string>): OllamaToo
return tools.filter((t) => allowed.has(t.function.name));
}
const READ_TOOLS = new Set([
'read_vault_file',
'search_vault_files',
]);
const READ_TOOLS = new Set(['read_vault_file', 'search_vault_files']);
const ORGANIZE_TOOLS = new Set([
'read_vault_file',
@@ -52,10 +49,7 @@ const EDIT_TOOLS = new Set([
'insert_link',
]);
const RESEARCH_TOOLS = new Set([
'read_vault_file',
'search_vault_files',
]);
const RESEARCH_TOOLS = new Set(['read_vault_file', 'search_vault_files']);
export const AGENT_MODE_CONFIGS: Record<AgentMode, AgentModeConfig> = {
ask: {
@@ -63,6 +57,8 @@ export const AGENT_MODE_CONFIGS: Record<AgentMode, AgentModeConfig> = {
description: 'Answer questions using vault context. Read-only mode.',
systemPrompt: `You are a helpful assistant that answers questions using the contents of the user's Obsidian vault.
You have access to search and read tools to find relevant information.
IMPORTANT: When you need vault information, do NOT say you will search or read files. You MUST immediately emit a tool_call to search_vault_files or read_vault_file.
Only respond to the user after you have received and analyzed the tool results.
Always base your answers on vault content when possible.
If you cannot find relevant information, say so clearly.
Do not make up facts.`,
@@ -76,6 +72,8 @@ Do not make up facts.`,
description: 'Create, modify, and organize notes with full editing tools.',
systemPrompt: `You are an assistant that helps edit and manage notes in the user's Obsidian vault.
You have full access to reading, searching, creating, appending, renaming, moving, and deleting notes.
IMPORTANT: When you need vault information, do NOT say you will search or read files. You MUST immediately emit a tool_call to the appropriate tool.
Only respond to the user after you have received and analyzed the tool results.
When editing notes:
- Prefer modifying existing content over creating duplicates.
- Use the replace_note_section tool to update specific sections.
@@ -92,6 +90,8 @@ When editing notes:
description: 'Tag, rename, move, and link notes to keep the vault tidy.',
systemPrompt: `You are an assistant that helps organize the user's Obsidian vault.
You can search notes, read them, update frontmatter tags, rename files, move files to folders, and insert wiki-links.
IMPORTANT: When you need vault information, do NOT say you will search or read files. You MUST immediately emit a tool_call to the appropriate tool.
Only respond to the user after you have received and analyzed the tool results.
When organizing:
- Suggest consistent tag vocabularies.
- Group related notes by linking them.
@@ -107,6 +107,8 @@ When organizing:
description: 'Deep vault search and synthesis across multiple notes.',
systemPrompt: `You are a research assistant that dives deep into the user's Obsidian vault.
Your job is to synthesize information across multiple notes, find connections, and produce comprehensive summaries.
IMPORTANT: When you need vault information, do NOT say you will search or read files. You MUST immediately emit a tool_call to search_vault_files or read_vault_file.
Only respond to the user after you have received and analyzed the tool results.
Search broadly, read key sources, and cross-reference information.
Cite specific notes and quotes where possible.
If information is incomplete or contradictory, note it explicitly.`,
+5 -39
View File
@@ -76,7 +76,9 @@ export class ChatView extends ItemView {
this.toolExecutor = new ToolExecutor(this.app.vault, this.app, telemetryManager);
this.actionPreviewBuilder = new ActionPreviewBuilder(this.app.vault, this.app);
this.noteContextBuilder = new NoteContextBuilder(this.app.vault, this.app, this.vaultIndexer);
this.conversationStateManager = new ConversationStateManager();
this.conversationStateManager = new ConversationStateManager(
getSystemPromptForMode(this.currentAgentMode)
);
this.structuredMemoryManager = structuredMemoryManager;
this.telemetryManager = telemetryManager;
this.workflowEngine = new WorkflowEngine(
@@ -365,11 +367,12 @@ export class ChatView extends ItemView {
if (this.modeSelectorEl) {
this.modeSelectorEl.value = mode;
}
this.conversationStateManager.setSystemPrompt(getSystemPromptForMode(mode));
}
clearConversation(): void {
this.messages = [];
this.conversationStateManager.clear();
this.conversationStateManager.clear(getSystemPromptForMode(this.currentAgentMode));
this.render();
}
@@ -623,43 +626,6 @@ export class ChatView extends ItemView {
return baseMessages;
}
buildMessages(userMessageContent: string, tools?: OllamaTool[]): OllamaMessage[] {
const systemContent = getSystemPromptForMode(this.currentAgentMode);
const messages: OllamaMessage[] = [];
// Inject structured memory as a preceding system message if available
if (this.structuredMemoryManager) {
const memoryContext = this.structuredMemoryManager.buildMemoryContext();
if (memoryContext) {
messages.push({
role: 'system',
content: memoryContext,
});
}
}
messages.push({
role: 'system',
content: systemContent,
});
const userMessage: OllamaMessage = {
role: 'user',
content: userMessageContent,
};
messages.push(userMessage);
if (tools && tools.length > 0) {
messages.push({
role: 'assistant',
content: 'I have access to the following tools to help answer your questions:',
});
}
return messages;
}
async processToolCalls(
toolCalls: OllamaToolCall[],
messages: OllamaMessage[],
+19 -20
View File
@@ -8,6 +8,10 @@ export interface ConversationState {
longTermContext: OllamaMessage[];
}
const DEFAULT_SYSTEM_PROMPT = `You are an assistant that can help answer questions using the contents of a vault.
When a user asks for information about their vault, you MUST call the search_vault_files or read_vault_file tool to find the answer.
Do not say you will search or read files — immediately emit the tool_call.`;
export class ConversationStateManager {
private shortTermContext: OllamaMessage[] = [];
private mediumTermContext: OllamaMessage[] = [];
@@ -15,15 +19,12 @@ export class ConversationStateManager {
private maxShortTermTurns: number = 10;
private maxMediumTermMessages: number = 20;
constructor() {
constructor(initialSystemPrompt?: string) {
// Initialize with default system context
this.longTermContext = [
{
role: 'system',
content: `You are an assistant that can help answer questions using the contents of a vault.
The user can ask questions about their vault contents, and you should provide helpful responses based on the files.
When a user asks for information, try to find relevant files using the search_vault_files tool and read their contents with the read_vault_file tool.
Only use the tools if you need to access vault content that is not already in the context.`,
content: initialSystemPrompt ?? DEFAULT_SYSTEM_PROMPT,
},
];
}
@@ -60,17 +61,18 @@ export class ConversationStateManager {
* Sets the user's persona or core knowledge as long-term context
* @param personaContent The persona or core knowledge content
*/
setPersona(personaContent: string): void {
// Remove any existing persona messages
this.longTermContext = this.longTermContext.filter(
(msg) =>
msg.role !== 'system' ||
!msg.content.includes(
'You are an assistant that can help answer questions using the contents of a vault'
)
);
setSystemPrompt(systemPrompt: string): void {
// Replace all existing system messages with the new system prompt
this.longTermContext = this.longTermContext.filter((msg) => msg.role !== 'system');
this.longTermContext.unshift({
role: 'system',
content: systemPrompt,
});
}
// Add the new persona
setPersona(personaContent: string): void {
// Replace all existing system messages with the new persona
this.longTermContext = this.longTermContext.filter((msg) => msg.role !== 'system');
this.longTermContext.push({
role: 'system',
content: personaContent,
@@ -117,16 +119,13 @@ export class ConversationStateManager {
/**
* Clears all conversation context
*/
clear(): void {
clear(systemPrompt?: string): void {
this.shortTermContext = [];
this.mediumTermContext = [];
this.longTermContext = [
{
role: 'system',
content: `You are an assistant that can help answer questions using the contents of a vault.
The user can ask questions about their vault contents, and you should provide helpful responses based on the files.
When a user asks for information, try to find relevant files using the search_vault_files tool and read their contents with the read_vault_file tool.
Only use the tools if you need to access vault content that is not already in the context.`,
content: systemPrompt ?? DEFAULT_SYSTEM_PROMPT,
},
];
}
+20 -22
View File
@@ -5,17 +5,18 @@ describe('ConversationStateManager', () => {
let manager: ConversationStateManager;
beforeEach(() => {
manager = new ConversationStateManager();
manager = new ConversationStateManager(
'You are an assistant that can help answer questions using the contents of a vault'
);
});
describe('constructor', () => {
it('should initialize with default system message in long-term context', () => {
const longTerm = manager.getLongTermContext();
const customManager = new ConversationStateManager();
const longTerm = customManager.getLongTermContext();
expect(longTerm).toHaveLength(1);
expect(longTerm[0].role).toBe('system');
expect(longTerm[0].content).toContain(
'You are an assistant that can help answer questions using the contents of a vault'
);
expect(longTerm[0].content).toContain('immediately emit the tool_call');
});
it('should initialize with empty short-term and medium-term contexts', () => {
@@ -64,25 +65,16 @@ describe('ConversationStateManager', () => {
it('should replace default system message with custom persona', () => {
manager.setPersona('You are a coding expert.');
const longTerm = manager.getLongTermContext();
expect(longTerm.some((msg) => msg.content === 'You are a coding expert.')).toBe(true);
expect(
longTerm.some((msg) =>
msg.content.includes(
'You are an assistant that can help answer questions using the contents of a vault'
)
)
).toBe(false);
expect(longTerm).toHaveLength(1);
expect(longTerm[0].content).toBe('You are a coding expert.');
});
it('should allow multiple persona updates', () => {
manager.setPersona('First persona.');
manager.setPersona('Second persona.');
const longTerm = manager.getLongTermContext();
expect(longTerm.some((msg) => msg.content === 'Second persona.')).toBe(true);
// The actual implementation filters out the default system message but keeps previous persona messages
// So we should expect to find both personas in the long-term context
expect(longTerm.some((msg) => msg.content === 'First persona.')).toBe(true);
expect(longTerm).toHaveLength(2);
expect(longTerm).toHaveLength(1);
expect(longTerm[0].content).toBe('Second persona.');
});
});
@@ -108,7 +100,7 @@ describe('ConversationStateManager', () => {
// Long-term comes first
expect(messages[0].role).toBe('system');
expect(messages[0].content).toContain('You are an assistant');
expect(messages[0].content).toContain('contents of a vault');
// Medium-term follows
expect(messages[1].content).toBe('Medium');
@@ -136,9 +128,14 @@ describe('ConversationStateManager', () => {
manager.setPersona('Custom persona');
manager.clear();
const longTerm = manager.getLongTermContext();
expect(longTerm[0].content).toContain(
'You are an assistant that can help answer questions using the contents of a vault'
);
expect(longTerm[0].content).toContain('immediately emit the tool_call');
});
it('should accept a custom system prompt when clearing', () => {
manager.setPersona('Custom persona');
manager.clear('Custom system prompt');
const longTerm = manager.getLongTermContext();
expect(longTerm[0].content).toBe('Custom system prompt');
});
});
@@ -209,6 +206,7 @@ describe('ConversationStateManager', () => {
const longTerm = manager.getLongTermContext();
expect(longTerm).toHaveLength(1);
expect(longTerm[0].role).toBe('system');
expect(longTerm[0].content).toContain('immediately emit the tool_call');
});
});
});