feat: add conditional web viewer access setting and directive system

Introduce a new setting to control AI access to the web viewer tool and refactor prompt building to inject capability directives (memories, web search, web viewer) into all agent contexts. Update tool definitions to conditionally include web viewer and memory tools based on settings. Consolidate memories injection and active directives into a unified prompt builder pattern.
This commit is contained in:
Andrew Beal 2026-04-10 22:19:34 +01:00
parent ac1dcf7f83
commit d52b1c3c74
16 changed files with 199 additions and 54 deletions

View file

@ -31,7 +31,7 @@ export abstract class AIToolDefinitions {
private static readonly definitionsList = [SearchVaultFiles, ReadVaultFiles, ListVaultFiles, GetWebViewerContent,
WriteVaultFile, PatchVaultFile, DeleteVaultFiles, MoveVaultFiles, CreateVaultFolder, DeleteVaultFolder, MoveVaultFolder];
public static agentDefinitions(destructive: boolean, planningMode: boolean, memories: boolean, updateMemories: boolean): IAIToolDefinition[] {
public static agentDefinitions(destructive: boolean, planningMode: boolean, memories: boolean, updateMemories: boolean, webViewer: boolean): IAIToolDefinition[] {
this.isGated = false;
if (planningMode) {
@ -41,10 +41,13 @@ export abstract class AIToolDefinitions {
let actions = [
SearchVaultFiles,
ReadVaultFiles,
ListVaultFiles,
GetWebViewerContent
ListVaultFiles
];
if (webViewer) {
actions = actions.concat([GetWebViewerContent]);
}
if (memories) {
actions = actions.concat([ReadMemories]);
}
@ -68,12 +71,11 @@ export abstract class AIToolDefinitions {
return actions;
}
public static orchestrationAgentDefinitions(): IAIToolDefinition[] {
return [
public static orchestrationAgentDefinitions(memories: boolean, webViewer: boolean): IAIToolDefinition[] {
let actions = [
SearchVaultFiles,
ReadVaultFiles,
ListVaultFiles,
GetWebViewerContent,
CompleteStep,
ReviseStep,
RevisePlan,
@ -81,10 +83,24 @@ export abstract class AIToolDefinitions {
CompletePlan,
CancelPlan
];
if (webViewer) {
actions = actions.concat([GetWebViewerContent]);
}
if (memories) {
actions = actions.concat([ReadMemories]);
}
return actions;
}
public static planningAgentDefinitions(memories: boolean): IAIToolDefinition[] {
let actions = [SearchVaultFiles, ReadVaultFiles, ListVaultFiles, GetWebViewerContent, AskUserQuestionPlanning, SubmitPlan];
public static planningAgentDefinitions(memories: boolean, webViewer: boolean): IAIToolDefinition[] {
let actions = [SearchVaultFiles, ReadVaultFiles, ListVaultFiles, AskUserQuestionPlanning, SubmitPlan];
if (webViewer) {
actions = actions.concat([GetWebViewerContent]);
}
if (memories) {
actions = actions.concat([ReadMemories]);
@ -94,7 +110,7 @@ export abstract class AIToolDefinitions {
}
public static executionAgentDefinitions(): IAIToolDefinition[] {
return [...this.agentDefinitions(true, false, false, false), CompleteTask];
return [...this.agentDefinitions(true, false, false, false, false), CompleteTask];
}
public static compactSummaryForPlanningAgent(): string {

View file

@ -11,8 +11,8 @@ import { Copy, replaceCopy } from "Enums/Copy";
export interface IPrompt {
systemInstruction(): Promise<string>;
orchestrationInstruction(): string;
planningInstruction(): string;
orchestrationInstruction(): Promise<string>;
planningInstruction(): Promise<string>;
executionInstruction(): string;
userInstruction(): Promise<string>;
}
@ -30,35 +30,57 @@ export class AIPrompt implements IPrompt {
}
public async systemInstruction(): Promise<string> {
let systemInstruction = SystemInstruction;
if (!this.settingsService.settings.enableMemories) {
return systemInstruction;
}
const memories = await this.memoriesService.readMemories();
if (memories !== "") {
systemInstruction = systemInstruction + replaceCopy(Copy.MemoriesInjectionHeader, [memories]);
}
if (!this.settingsService.settings.allowUpdatingMemories) {
systemInstruction = systemInstruction + Copy.MemoriesReadOnly;
}
return systemInstruction;
return this.buildPrompt(SystemInstruction);
}
public orchestrationInstruction(): string {
return OrchestrationPrompt;
public async orchestrationInstruction(): Promise<string> {
return this.buildPrompt(OrchestrationPrompt);
}
public planningInstruction(): string {
return PlanningPrompt;
public async planningInstruction(): Promise<string> {
return this.buildPrompt(PlanningPrompt);
}
public executionInstruction(): string {
return ExecutionPrompt;
}
private async buildPrompt(basePrompt: string): Promise<string> {
let prompt = basePrompt;
if (this.settingsService.settings.enableMemories) {
const memories = await this.memoriesService.readMemories();
if (memories !== "") {
prompt = prompt + replaceCopy(Copy.MemoriesInjectionHeader, [memories]);
}
}
prompt = prompt + this.buildActiveDirectives();
return prompt;
}
private buildActiveDirectives(): string {
const s = this.settingsService.settings;
const memoriesDirective = !s.enableMemories
? Copy.DirectiveMemoriesDisabled
: s.allowUpdatingMemories
? Copy.DirectiveMemoriesEnabled
: Copy.DirectiveMemoriesReadOnly;
const webSearchDirective = s.enableWebSearch
? Copy.DirectiveWebSearchEnabled
: Copy.DirectiveWebSearchDisabled;
const webViewerDirective = s.enableWebViewer
? Copy.DirectiveWebViewerEnabled
: Copy.DirectiveWebViewerDisabled;
const directives = [memoriesDirective, webSearchDirective, webViewerDirective].join("\n");
return replaceCopy(Copy.ActiveCapabilitiesHeader, [directives]);
}
public async userInstruction(): Promise<string> {
const result = await this.fileSystemService.readFile(this.settingsService.settings.userInstruction, true);
return result instanceof Error ? "" : result;

View file

@ -106,6 +106,25 @@ Cancel only after you've exhausted recovery options and confirmed the goal is un
| "Search returned no results" | Try alternative search terms yourself. If still nothing, the information may not exist adjust the plan accordingly. |
| "Content structure unexpected" | Read the file yourself to understand the actual structure, then revise the step with accurate instructions. |
## Memories
If memories are enabled, the current memory content is injected into your context immediately after this system prompt. Memories record vault conventions, user preferences, folder structures, and other facts established in previous sessions.
Use memories as background context when evaluating step outcomes for example, a step that references a path or convention recorded in memory is more likely to be correct than one that contradicts it.
Memories are read-only for the orchestration agent do not attempt to update them.
Check the **Active Capabilities** section at the end of this prompt to see whether memories are enabled.
## Web Search and Web Viewer
The execution agent may have access to web search and web viewer tools, depending on the user's settings. This is relevant when evaluating failed steps:
- If a step failed because it attempted to use a web search or web viewer tool that is not available, this is a configuration issue do not retry the step as-is; consult the user instead
- If web search or web viewer is enabled and a step failed for another reason, normal recovery applies
Check the **Active Capabilities** section at the end of this prompt to see which tools are currently enabled.
## Vault Access
You can search and read vault files at any point before making a decision. This is your primary advantage over the execution agent, which operates with only its single-step context.

View file

@ -298,6 +298,39 @@ ${AIToolDefinitions.compactSummaryForPlanningAgent()}
Design your plan steps around these capabilities. The execution agent selects the appropriate tools during execution.
## Memories
If memories are enabled, the current memory content is injected into your context immediately after this system prompt. Memories record vault conventions, user preferences, folder structures, templates, and other facts established in previous sessions.
**Use memories when planning:**
- Apply recorded conventions when deciding file paths, naming patterns, and folder structure
- Reference known templates or workflows instead of designing from scratch
- Let recorded user preferences guide decisions about formatting, organisation, and linking
Memories are read-only for the planning agent do not attempt to update them.
Check the **Active Capabilities** section at the end of this prompt to see whether memories are enabled.
## Web Search
The execution agent may have access to a web search tool. Whether it is available depends on the user's current settings.
**Factor web search availability into your plans:**
- If web search is enabled, you may include steps that require the execution agent to retrieve current information from the web
- If web search is disabled, do not plan steps that depend on it work with vault content and general knowledge only
Check the **Active Capabilities** section at the end of this prompt to see whether web search is enabled.
## Web Viewer
The execution agent may have access to a web viewer tool that retrieves the content of a page currently open in the Obsidian web viewer. This is NOT general web browsing it reads a specific page the user already has open.
**Factor web viewer availability into your plans:**
- If web viewer is enabled, you may include steps that retrieve content from an open web page
- If web viewer is disabled, do not plan steps that depend on it
Check the **Active Capabilities** section at the end of this prompt to see whether web viewer is enabled.
## Common Anti-Patterns
### Vague Goal Steps

View file

@ -230,8 +230,9 @@ The memory file is injected into your context at the start of every session, imm
- **Injected**: You do not need to fetch or search for the memory file it is always already present in your context when a session begins.
- **Editable**: You can update memories, including adding, deleting or editing existing memories.
- **Scoped to this vault**: Memory is not global it reflects this user's specific vault structure, conventions, and preferences.
- **Optional**: There may not yet be any recorded memories or the user may have chosen to disable memories
- **Atomic if updates lare disabled**: The user has the ability to prevent updates to memories. In this instance memories are avalable to read but not to write.
- **Optional**: There may not yet be any recorded memories or the user may have chosen to disable memories.
- **Read-only when updates are disabled**: The user has the ability to prevent updates to memories. In this instance memories are available to read but not to write.
- **Current status**: The **Active Capabilities** section at the end of this prompt states whether memories are enabled and whether updates are permitted.
#### The Read-Before-Write Rule
@ -478,9 +479,15 @@ After multi-step execution:
- Problem-solving and explanations across any domain
- Programming, writing, and creative tasks with vault context
**Web Search**
- You may have the ability to search the web for current information
- Check the **Active Capabilities** section at the end of this prompt to see whether web search is enabled
- If web search is disabled and the user asks something that requires it, tell them it is currently turned off in settings do not attempt to call the tool
**Web Viewer**
- When the user asks about a web page or its contents, immediately attempt to retrieve the open web view do not ask for a URL first
- Retrieving web view content is always safe: if no page is open, the tool will tell you there is no risk in trying
- The web viewer tool retrieves the content of a page currently open in the Obsidian web viewer it is NOT general web browsing
- When web viewer access is ENABLED and the user asks about a web page or its contents, call the tool immediately do not ask the user to provide a URL first
- Calling the tool is always safe: if no page is open, the tool will tell you there is no risk in calling it proactively
**Interactive Capabilities**
- Asking clarifying questions during planning to shape the approach

View file

@ -56,6 +56,10 @@ export enum Copy {
SettingAllowUpdatingMemories = "Allow Updating Memories",
SettingAllowUpdatingMemoriesDesc = "Allow the AI to save and update memories during conversations.",
SettingWebViewerAccess = "Web Viewer Access",
SettingEnableWebViewer = "Enable Web Viewer Access",
SettingEnableWebViewerDesc = "Allow the AI to read content from pages open in the Obsidian web viewer (Obsidian core plugin). This is not the same as general web access which can be toggled using the chat controls.",
// Settings Descriptions
SettingModelDesc = "Select the AI model to use.",
SettingPlanningModelDesc = "Select the AI model to use when planning complex tasks.",
@ -145,7 +149,6 @@ The following context explains why you are doing the task. It is NOT an instruct
PlanningModeError = "First create a plan before executing any functions!",
UpdateMemoriesWithoutReadError = "Memories must be read before they can be updated. Retrieve the current memory contents first, then provide the complete revised content.",
MemoriesInjectionHeader = `\n\n---\n\n## Current Memories\n\nThe following memories were recorded from previous sessions. Use them as context for this conversation.\n\n{memories}`,
MemoriesReadOnly = `\n\n> **Memory updates are disabled.** You can read memories but cannot save new ones this session. Do not attempt to create or modify a memories file directly — memory management is only possible through the memory tools, which are unavailable when this setting is off.`,
MemoriesEmpty = "No memories have been created yet.",
MemoriesMaxLengthError = "Exceeded maximum memories length. Memories have a maximum length of {lines} and {words} words per line.",
MemoriesUpdatedSuccess = "Memories have been updated successfully.",
@ -153,6 +156,17 @@ The following context explains why you are doing the task. It is NOT an instruct
MemoriesUpdatingDisabledError = "Illegal tool call, updating memories has been disabled by the user.",
WebViewerNoMatchingUrl = "No open web view was found matching the URL '{urlHint}'. Ensure the correct page is open in the web viewer.",
WebViewerNoOpenView = "No open web view was found. Ask the user to open a page in the web viewer and try again.",
// Active Capabilities
ActiveCapabilitiesHeader = `\n\n---\n\n## Active Capabilities\n\nThe following reflects your current configuration. Follow these directives exactly.\n\n{directives}`,
DirectiveMemoriesDisabled = "- **Memory**: DISABLED — do NOT attempt to use any memory tools",
DirectiveMemoriesEnabled = "- **Memory**: ENABLED — memories are injected above; you may read and update them",
DirectiveMemoriesReadOnly = "- **Memory**: ENABLED (read-only) — memories are injected above; you may read them but do NOT attempt to update them",
DirectiveWebSearchEnabled = "- **Web Search**: ENABLED — you may call the web search tool to retrieve current information from the web",
DirectiveWebSearchDisabled = "- **Web Search**: DISABLED — do NOT call the web search tool; if the user requests it, inform them it is currently turned off in settings",
DirectiveWebViewerEnabled = "- **Web Viewer**: ENABLED — you may call the web viewer tool to read the content of the page currently open in the Obsidian web viewer; call it proactively when the user asks about a web page",
DirectiveWebViewerDisabled = "- **Web Viewer**: DISABLED — do NOT call the web viewer tool",
PlanSubmissionRequired = "Error: Attempted to exit planning but plan has not yet been submitted!",
MaxExecutionDepthReached = "Exceeded maximum plan execution attempts - consult with the user on how to continue.",

View file

@ -45,6 +45,10 @@ export abstract class BaseAgent {
this.onSaveConversation = callback;
}
protected webViewerAccessEnabled(): boolean {
return this.settingsService.settings.enableWebViewer;
}
protected memoriesEnabled(): boolean {
return this.settingsService.settings.enableMemories;
}

View file

@ -93,8 +93,8 @@ export class MainAgent extends BaseAgent {
this.ai.aiToolUsageMode = AIToolUsageMode.Auto;
this.ai.systemPrompt = await this.aiPrompt.systemInstruction();
this.ai.userInstruction = await this.aiPrompt.userInstruction();
this.ai.aiToolDefinitions = AIToolDefinitions.agentDefinitions(
allowDestructiveActions, planningMode, this.memoriesEnabled(), this.updateMemoriesEnabled());
this.ai.aiToolDefinitions = AIToolDefinitions.agentDefinitions(allowDestructiveActions, planningMode,
this.memoriesEnabled(), this.updateMemoriesEnabled(), this.webViewerAccessEnabled());
}
protected override setDebugColor(): void {

View file

@ -16,6 +16,7 @@ import { AIToolResponse } from "AIClasses/ToolDefinitions/AIToolResponse";
import { DebugColor } from "Enums/DebugColor";
import { AIToolUsageMode } from "Enums/AIToolUsageMode";
import { AIToolResponsePayload } from "AIClasses/ToolDefinitions/AIToolResponsePayload";
import type { IAIToolDefinition } from "AIClasses/ToolDefinitions/IAIToolDefinition";
export class OrchestrationAgent extends BaseAgent {
@ -149,7 +150,7 @@ export class OrchestrationAgent extends BaseAgent {
}
private async runOrchestrationAgentLoop(planningConversation: Conversation, callbacks: IChatServiceCallbacks): Promise<OrchestrationResult> {
this.setAgentPromptAndTools();
await this.setAgentPromptAndTools();
if (this.orchestrationDepth >= OrchestrationAgent.MAX_AGENT_DEPTH) {
this.debugService?.log("Orchestration", "Max orchestration depth reached - aborting");
@ -163,7 +164,7 @@ export class OrchestrationAgent extends BaseAgent {
await this.runAgentLoop(AgentType.Orchestration, planningConversation, callbacks, async toolCall => {
const toolCallName = toolCall.name;
if (!AIToolDefinitions.orchestrationAgentDefinitions().some(definition => isAITool(toolCallName, definition.name))) {
if (!this.orchestrationTools().some(definition => isAITool(toolCallName, definition.name))) {
this.debugService?.log("Orchestration", `Invalid tool call denied: ${toolCallName}`);
planningConversation.addFunctionResponse(new AIToolResponse(
toolCallName,
@ -356,15 +357,19 @@ export class OrchestrationAgent extends BaseAgent {
return input.goal + context;
}
private setAgentPromptAndTools(): void {
private async setAgentPromptAndTools(): Promise<void> {
if (!this.ai) {
Exception.throw("Error: No AI provider has been set!");
}
this.ai.agentType = AgentType.Orchestration;
this.ai.aiToolUsageMode = AIToolUsageMode.Enabled;
this.ai.systemPrompt = this.aiPrompt.orchestrationInstruction();
this.ai.systemPrompt = await this.aiPrompt.orchestrationInstruction();
this.ai.userInstruction = ""; // do not include user instruction for orchestration agent
this.ai.aiToolDefinitions = AIToolDefinitions.orchestrationAgentDefinitions();
this.ai.aiToolDefinitions = this.orchestrationTools();
}
private orchestrationTools(): IAIToolDefinition[] {
return AIToolDefinitions.orchestrationAgentDefinitions(this.memoriesEnabled(), this.webViewerAccessEnabled());
}
protected override setDebugColor(): void {

View file

@ -14,6 +14,7 @@ import { Role } from "Enums/Role";
import { DebugColor } from "Enums/DebugColor";
import { AIToolUsageMode } from "Enums/AIToolUsageMode";
import { AIToolResponsePayload } from "AIClasses/ToolDefinitions/AIToolResponsePayload";
import type { IAIToolDefinition } from "AIClasses/ToolDefinitions/IAIToolDefinition";
export class PlanningAgent extends BaseAgent {
@ -22,7 +23,7 @@ export class PlanningAgent extends BaseAgent {
private planningDepth: number = 0;
public async runPlanningAgent(conversation: Conversation, callbacks: IChatServiceCallbacks): Promise<ExecutionPlan | undefined> {
this.setAgentPromptAndTools();
await this.setAgentPromptAndTools();
let capturedPlan: ExecutionPlan | null = null;
@ -36,7 +37,7 @@ export class PlanningAgent extends BaseAgent {
await this.runAgentLoop(AgentType.Planning, conversation, callbacks, async (toolCall) => {
const toolCallName = toolCall.name;
if (!AIToolDefinitions.planningAgentDefinitions(this.memoriesEnabled()).some(definition => isAITool(toolCallName, definition.name))) {
if (!this.planningTools().some(definition => isAITool(toolCallName, definition.name))) {
this.debugService?.log("PlanningAgent", `Invalid tool call denied: ${toolCallName}`);
conversation.addFunctionResponse(new AIToolResponse(
toolCallName,
@ -107,15 +108,19 @@ export class PlanningAgent extends BaseAgent {
return capturedPlan;
}
private setAgentPromptAndTools() {
private async setAgentPromptAndTools() {
if (!this.ai) { // this shouldn't ever happen
Exception.throw("Error: No AI provider has been set!");
}
this.ai.agentType = AgentType.Planning;
this.ai.aiToolUsageMode = AIToolUsageMode.Enabled;
this.ai.systemPrompt = this.aiPrompt.planningInstruction();
this.ai.systemPrompt = await this.aiPrompt.planningInstruction();
this.ai.userInstruction = ""; // do not include user instruction for planning agent
this.ai.aiToolDefinitions = AIToolDefinitions.planningAgentDefinitions(this.memoriesEnabled());
this.ai.aiToolDefinitions = this.planningTools();
}
private planningTools(): IAIToolDefinition[] {
return AIToolDefinitions.planningAgentDefinitions(this.memoriesEnabled(), this.webViewerAccessEnabled());
}
protected override setDebugColor(): void {

View file

@ -23,7 +23,8 @@ const DEFAULT_SETTINGS: IVaultkeeperAISettings = {
enableMemories: false,
allowUpdatingMemories: true,
enableWebSearch: true
enableWebSearch: true,
enableWebViewer: false
}
export interface IVaultkeeperAISettings {
@ -47,6 +48,7 @@ export interface IVaultkeeperAISettings {
allowUpdatingMemories: boolean;
enableWebSearch: boolean;
enableWebViewer: boolean;
}
export class SettingsService {

View file

@ -174,6 +174,24 @@ export class VaultkeeperAISettingTab extends PluginSettingTab {
});
});
/* Web Access Header */
new Setting(containerEl)
.setHeading()
.setName(Copy.SettingWebViewerAccess);
/* Enable Web Viewer Setting */
new Setting(containerEl)
.setName(Copy.SettingEnableWebViewer)
.setDesc(Copy.SettingEnableWebViewerDesc)
.addToggle(toggle => {
toggle
.setValue(this.settingsService.settings.enableWebViewer)
.onChange(async (value) => {
this.settingsService.settings.enableWebViewer = value;
await this.settingsService.saveSettings();
});
});
/* Memories Header */
new Setting(containerEl)
.setHeading()

View file

@ -47,7 +47,7 @@ describe('AIControllerService - Integration Tests', () => {
mockPrompt = {
systemInstruction: vi.fn().mockReturnValue('System instruction'),
userInstruction: vi.fn().mockResolvedValue('User instruction'),
planningInstruction: vi.fn().mockReturnValue('Planning instruction')
planningInstruction: vi.fn().mockResolvedValue('Planning instruction')
};
RegisterSingleton(Services.IPrompt, mockPrompt);

View file

@ -53,8 +53,8 @@ describe('Multi-Agent Integration Tests', () => {
mockPrompt = {
systemInstruction: vi.fn().mockReturnValue('System instruction'),
userInstruction: vi.fn().mockResolvedValue('User instruction'),
planningInstruction: vi.fn().mockReturnValue('Planning instruction'),
orchestrationInstruction: vi.fn().mockReturnValue('Orchestration instruction'),
planningInstruction: vi.fn().mockResolvedValue('Planning instruction'),
orchestrationInstruction: vi.fn().mockResolvedValue('Orchestration instruction'),
executionInstruction: vi.fn().mockReturnValue('Execution instruction')
};
RegisterSingleton(Services.IPrompt, mockPrompt);

View file

@ -28,8 +28,8 @@ describe('OrchestrationAgent - Unit Tests', () => {
mockPrompt = {
systemInstruction: vi.fn().mockReturnValue('System instruction'),
userInstruction: vi.fn().mockResolvedValue('User instruction'),
planningInstruction: vi.fn().mockReturnValue('Planning instruction'),
orchestrationInstruction: vi.fn().mockReturnValue('Orchestration instruction'),
planningInstruction: vi.fn().mockResolvedValue('Planning instruction'),
orchestrationInstruction: vi.fn().mockResolvedValue('Orchestration instruction'),
executionInstruction: vi.fn().mockReturnValue('Execution instruction')
};
RegisterSingleton(Services.IPrompt, mockPrompt);

View file

@ -47,7 +47,7 @@ describe('PlanningAgent - Unit Tests', () => {
mockPrompt = {
systemInstruction: vi.fn().mockReturnValue('System instruction'),
userInstruction: vi.fn().mockResolvedValue('User instruction'),
planningInstruction: vi.fn().mockReturnValue('Planning instruction')
planningInstruction: vi.fn().mockResolvedValue('Planning instruction')
};
RegisterSingleton(Services.IPrompt, mockPrompt);