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)); return tools.filter((t) => allowed.has(t.function.name));
} }
const READ_TOOLS = new Set([ const READ_TOOLS = new Set(['read_vault_file', 'search_vault_files']);
'read_vault_file',
'search_vault_files',
]);
const ORGANIZE_TOOLS = new Set([ const ORGANIZE_TOOLS = new Set([
'read_vault_file', 'read_vault_file',
@@ -52,10 +49,7 @@ const EDIT_TOOLS = new Set([
'insert_link', 'insert_link',
]); ]);
const RESEARCH_TOOLS = new Set([ const RESEARCH_TOOLS = new Set(['read_vault_file', 'search_vault_files']);
'read_vault_file',
'search_vault_files',
]);
export const AGENT_MODE_CONFIGS: Record<AgentMode, AgentModeConfig> = { export const AGENT_MODE_CONFIGS: Record<AgentMode, AgentModeConfig> = {
ask: { ask: {
@@ -63,6 +57,8 @@ export const AGENT_MODE_CONFIGS: Record<AgentMode, AgentModeConfig> = {
description: 'Answer questions using vault context. Read-only mode.', 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. 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. 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. Always base your answers on vault content when possible.
If you cannot find relevant information, say so clearly. If you cannot find relevant information, say so clearly.
Do not make up facts.`, Do not make up facts.`,
@@ -76,6 +72,8 @@ Do not make up facts.`,
description: 'Create, modify, and organize notes with full editing tools.', 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. 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. 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: When editing notes:
- Prefer modifying existing content over creating duplicates. - Prefer modifying existing content over creating duplicates.
- Use the replace_note_section tool to update specific sections. - 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.', 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. 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. 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: When organizing:
- Suggest consistent tag vocabularies. - Suggest consistent tag vocabularies.
- Group related notes by linking them. - Group related notes by linking them.
@@ -107,6 +107,8 @@ When organizing:
description: 'Deep vault search and synthesis across multiple notes.', description: 'Deep vault search and synthesis across multiple notes.',
systemPrompt: `You are a research assistant that dives deep into the user's Obsidian vault. 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. 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. Search broadly, read key sources, and cross-reference information.
Cite specific notes and quotes where possible. Cite specific notes and quotes where possible.
If information is incomplete or contradictory, note it explicitly.`, 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.toolExecutor = new ToolExecutor(this.app.vault, this.app, telemetryManager);
this.actionPreviewBuilder = new ActionPreviewBuilder(this.app.vault, this.app); this.actionPreviewBuilder = new ActionPreviewBuilder(this.app.vault, this.app);
this.noteContextBuilder = new NoteContextBuilder(this.app.vault, this.app, this.vaultIndexer); 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.structuredMemoryManager = structuredMemoryManager;
this.telemetryManager = telemetryManager; this.telemetryManager = telemetryManager;
this.workflowEngine = new WorkflowEngine( this.workflowEngine = new WorkflowEngine(
@@ -365,11 +367,12 @@ export class ChatView extends ItemView {
if (this.modeSelectorEl) { if (this.modeSelectorEl) {
this.modeSelectorEl.value = mode; this.modeSelectorEl.value = mode;
} }
this.conversationStateManager.setSystemPrompt(getSystemPromptForMode(mode));
} }
clearConversation(): void { clearConversation(): void {
this.messages = []; this.messages = [];
this.conversationStateManager.clear(); this.conversationStateManager.clear(getSystemPromptForMode(this.currentAgentMode));
this.render(); this.render();
} }
@@ -623,43 +626,6 @@ export class ChatView extends ItemView {
return baseMessages; 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( async processToolCalls(
toolCalls: OllamaToolCall[], toolCalls: OllamaToolCall[],
messages: OllamaMessage[], messages: OllamaMessage[],
+19 -20
View File
@@ -8,6 +8,10 @@ export interface ConversationState {
longTermContext: OllamaMessage[]; 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 { export class ConversationStateManager {
private shortTermContext: OllamaMessage[] = []; private shortTermContext: OllamaMessage[] = [];
private mediumTermContext: OllamaMessage[] = []; private mediumTermContext: OllamaMessage[] = [];
@@ -15,15 +19,12 @@ export class ConversationStateManager {
private maxShortTermTurns: number = 10; private maxShortTermTurns: number = 10;
private maxMediumTermMessages: number = 20; private maxMediumTermMessages: number = 20;
constructor() { constructor(initialSystemPrompt?: string) {
// Initialize with default system context // Initialize with default system context
this.longTermContext = [ this.longTermContext = [
{ {
role: 'system', role: 'system',
content: `You are an assistant that can help answer questions using the contents of a vault. content: initialSystemPrompt ?? DEFAULT_SYSTEM_PROMPT,
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.`,
}, },
]; ];
} }
@@ -60,17 +61,18 @@ export class ConversationStateManager {
* Sets the user's persona or core knowledge as long-term context * Sets the user's persona or core knowledge as long-term context
* @param personaContent The persona or core knowledge content * @param personaContent The persona or core knowledge content
*/ */
setPersona(personaContent: string): void { setSystemPrompt(systemPrompt: string): void {
// Remove any existing persona messages // Replace all existing system messages with the new system prompt
this.longTermContext = this.longTermContext.filter( this.longTermContext = this.longTermContext.filter((msg) => msg.role !== 'system');
(msg) => this.longTermContext.unshift({
msg.role !== 'system' || role: 'system',
!msg.content.includes( content: systemPrompt,
'You are an assistant that can help answer questions using the contents of a vault' });
) }
);
// 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({ this.longTermContext.push({
role: 'system', role: 'system',
content: personaContent, content: personaContent,
@@ -117,16 +119,13 @@ export class ConversationStateManager {
/** /**
* Clears all conversation context * Clears all conversation context
*/ */
clear(): void { clear(systemPrompt?: string): void {
this.shortTermContext = []; this.shortTermContext = [];
this.mediumTermContext = []; this.mediumTermContext = [];
this.longTermContext = [ this.longTermContext = [
{ {
role: 'system', role: 'system',
content: `You are an assistant that can help answer questions using the contents of a vault. content: systemPrompt ?? DEFAULT_SYSTEM_PROMPT,
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.`,
}, },
]; ];
} }
+20 -22
View File
@@ -5,17 +5,18 @@ describe('ConversationStateManager', () => {
let manager: ConversationStateManager; let manager: ConversationStateManager;
beforeEach(() => { beforeEach(() => {
manager = new ConversationStateManager(); manager = new ConversationStateManager(
'You are an assistant that can help answer questions using the contents of a vault'
);
}); });
describe('constructor', () => { describe('constructor', () => {
it('should initialize with default system message in long-term context', () => { 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).toHaveLength(1);
expect(longTerm[0].role).toBe('system'); expect(longTerm[0].role).toBe('system');
expect(longTerm[0].content).toContain( expect(longTerm[0].content).toContain('immediately emit the tool_call');
'You are an assistant that can help answer questions using the contents of a vault'
);
}); });
it('should initialize with empty short-term and medium-term contexts', () => { 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', () => { it('should replace default system message with custom persona', () => {
manager.setPersona('You are a coding expert.'); manager.setPersona('You are a coding expert.');
const longTerm = manager.getLongTermContext(); const longTerm = manager.getLongTermContext();
expect(longTerm.some((msg) => msg.content === 'You are a coding expert.')).toBe(true); expect(longTerm).toHaveLength(1);
expect( expect(longTerm[0].content).toBe('You are a coding expert.');
longTerm.some((msg) =>
msg.content.includes(
'You are an assistant that can help answer questions using the contents of a vault'
)
)
).toBe(false);
}); });
it('should allow multiple persona updates', () => { it('should allow multiple persona updates', () => {
manager.setPersona('First persona.'); manager.setPersona('First persona.');
manager.setPersona('Second persona.'); manager.setPersona('Second persona.');
const longTerm = manager.getLongTermContext(); const longTerm = manager.getLongTermContext();
expect(longTerm.some((msg) => msg.content === 'Second persona.')).toBe(true); expect(longTerm).toHaveLength(1);
// The actual implementation filters out the default system message but keeps previous persona messages expect(longTerm[0].content).toBe('Second persona.');
// 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);
}); });
}); });
@@ -108,7 +100,7 @@ describe('ConversationStateManager', () => {
// Long-term comes first // Long-term comes first
expect(messages[0].role).toBe('system'); 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 // Medium-term follows
expect(messages[1].content).toBe('Medium'); expect(messages[1].content).toBe('Medium');
@@ -136,9 +128,14 @@ describe('ConversationStateManager', () => {
manager.setPersona('Custom persona'); manager.setPersona('Custom persona');
manager.clear(); manager.clear();
const longTerm = manager.getLongTermContext(); const longTerm = manager.getLongTermContext();
expect(longTerm[0].content).toContain( expect(longTerm[0].content).toContain('immediately emit the tool_call');
'You are an assistant that can help answer questions using the contents of a vault' });
);
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(); const longTerm = manager.getLongTermContext();
expect(longTerm).toHaveLength(1); expect(longTerm).toHaveLength(1);
expect(longTerm[0].role).toBe('system'); expect(longTerm[0].role).toBe('system');
expect(longTerm[0].content).toContain('immediately emit the tool_call');
}); });
}); });
}); });