Add context passing between execution steps and enhance orchestration

- Add context_for_next_step parameter to CompleteStep for passing execution history
- Add context parameter to CompleteTask for preserving task completion state
- Update OrchestrationResult to handle context propagation between steps
- Add debug color differentiation for agent types (Main, Execution, Orchestration, Planning)
- Reorganize SearchTypes from Helpers to Types directory
- Add justification requirement for execution deviations
- Support reasonable deviations in orchestration plan validation
- Refactor dependency service with TryResolve utility
- Add whitespace cleanup to Semaphore class
This commit is contained in:
Andrew Beal 2026-01-28 21:23:47 +00:00
parent ab9ee08281
commit cefc408b2e
26 changed files with 529 additions and 337 deletions

View file

@ -11,10 +11,14 @@ export const CompleteStep: IAIFunctionDefinition = {
parameters: {
type: "object",
properties: {
context_for_next_step: {
type: "string",
description: "Optional contextual information needed for the next step's execution, derived from any relevant parts of the execution history so far. Analyze what the next step requires and provide only the pertinent information from previous steps (not just the most recent one). Examples: 'Files created: /notes/summary.md, /notes/details.md', 'Search found 5 entries in tags: work, personal', 'Current state: theme=dark, sidebar=collapsed'. Omit if the next step can execute independently without prior results."
},
confirm_completion: {
type: "boolean",
description: "Safety flag that must be explicitly set to true to confirm the step completion is intentional. This prevents accidental completions.",
default: false
type: "boolean",
description: "Safety flag that must be explicitly set to true to confirm the step completion is intentional. This prevents accidental completions.",
default: false
}
},
required: ["confirm_completion"]

View file

@ -22,6 +22,10 @@ export const CompleteTask: IAIFunctionDefinition = {
description: {
type: "string",
description: "A summary of what was accomplished, any outputs produced, or what prevented completion."
},
context: {
type: "string",
description: "Optional contextual information that should be preserved or passed forward after task completion. Include any relevant state, findings, file paths, or intermediate results that might be useful for subsequent operations or for the user to understand what was done. Examples: 'Created 3 new notes in /Projects folder', 'Found configuration in settings.json', 'Modified files: note1.md, note2.md'"
}
},
required: ["success", "description"]

View file

@ -75,12 +75,14 @@ export const CancelPlanArgsSchema = z.object({
});
export const CompleteStepArgsSchema = z.object({
context_for_next_step: z.string().optional(),
confirm_completion: z.boolean()
});
export const CompleteTaskArgsSchema = z.object({
success: z.boolean(),
description: z.string()
description: z.string(),
context: z.string().optional()
});
export const SubmitPlanArgsSchema = z.object({

View file

@ -29,6 +29,7 @@ If you cannot complete the task:
- 2. Document specifically what blocked you
- 3. Signal completion with a clear indication of failure and explanation of the blocker
- 4. Do not leave work in a broken or half-finished state if avoidable
- 5. If you deviated from the initial task instructions, justify why you did this
### Boundaries
- You have no memory of previous conversations

View file

@ -16,6 +16,7 @@ After reviewing the execution state, you must signal exactly one outcome:
Signal this when:
- The most recent step succeeded
- The results align with what the plan expected
- The results don't align with what the plan expected but the results have been reasonably justified
- No adjustments are needed
**Replan** The plan needs adjustment.

9
Enums/DebugColor.ts Normal file
View file

@ -0,0 +1,9 @@
export enum DebugColor {
RED = "#F44336",
ORANGE = "#FF9800",
YELLOW = "#FFEB3B",
GREEN = "#4CAF50",
BLUE = "#2196F3",
PINK = "#E91E63",
WHITE = "#FFFFFF"
}

View file

@ -1,5 +1,5 @@
import { extractText, getDocumentProxy } from 'unpdf';
import type { IPageText } from './SearchTypes';
import type { IPageText } from '../Types/SearchTypes';
import { Exception } from './Exception';
export async function readPDF(arrayBuffer: ArrayBuffer): Promise<IPageText[]> {

View file

@ -1,4 +1,5 @@
export class Semaphore {
private max: number;
private count: number;
private readonly waitAsync: boolean;
@ -38,4 +39,5 @@ export class Semaphore {
}
}
}
}

View file

@ -9,21 +9,26 @@ import { Copy } from "Enums/Copy";
import { Role } from "Enums/Role";
import { sanitizeFunctionCallContent } from "Helpers/ResponseHelper";
import type { IChatServiceCallbacks } from "Services/ChatService";
import { Resolve } from "Services/DependencyService";
import { Resolve, TryResolve } from "Services/DependencyService";
import { Services } from "Services/Services";
import type { AIFunctionService } from "./AIFunctionService";
import type { DebugService } from "Services/DebugService";
import { DebugColor } from "Enums/DebugColor";
export class AIController {
protected ai: IAIClass | undefined;
protected readonly aiPrompt: IPrompt;
protected readonly aiFunctionService: AIFunctionService;
protected readonly debugService: DebugService | undefined;
private onSaveConversation?: (conversation: Conversation) => Promise<void>;
public constructor() {
this.aiPrompt = Resolve<IPrompt>(Services.IPrompt);
this.aiFunctionService = Resolve<AIFunctionService>(Services.AIFunctionService);
this.debugService = TryResolve<DebugService>(Services.DebugService);
this.setDebugColor();
}
public resolveAIProvider() {
@ -91,6 +96,10 @@ export class AIController {
}
}
protected setDebugColor() {
this.debugService?.setDebugColor(DebugColor.WHITE);
}
private async saveConversation(agentType: AgentType, conversation: Conversation) {
if (agentType === AgentType.Main) {
await this.onSaveConversation?.(conversation);

View file

@ -4,7 +4,7 @@ import type { FileSystemService } from "../FileSystemService";
import { AIFunction, fromString } from "Enums/AIFunction";
import { AIFunctionResponse } from "AIClasses/FunctionDefinitions/AIFunctionResponse";
import type { AIFunctionCall } from "AIClasses/AIFunctionCall";
import type { ISearchMatch } from "../../Helpers/SearchTypes";
import type { ISearchMatch } from "../../Types/SearchTypes";
import { AbortService } from "../AbortService";
import { normalizePath, TAbstractFile, TFile } from "obsidian";
import { Exception } from "Helpers/Exception";

View file

@ -11,6 +11,7 @@ import { AIFunctionResponse } from "AIClasses/FunctionDefinitions/AIFunctionResp
import { Exception } from "Helpers/Exception";
import { AIFunctionDefinitions } from "AIClasses/FunctionDefinitions/AIFunctionDefinitions";
import { Copy, replaceCopy } from "Enums/Copy";
import { DebugColor } from "Enums/DebugColor";
export class ExecutionAgent extends AIController {
@ -80,4 +81,8 @@ export class ExecutionAgent extends AIController {
this.ai.toolDefinitions = AIFunctionDefinitions.executionAgentDefinitions();
}
protected override setDebugColor(): void {
this.debugService?.setDebugColor(DebugColor.GREEN);
}
}

View file

@ -11,6 +11,7 @@ import type { AIFunctionCall } from "AIClasses/AIFunctionCall";
import { OrchestrationAgent } from "./OrchestrationAgent";
import { ConversationContent } from "Conversations/ConversationContent";
import { Role } from "Enums/Role";
import { DebugColor } from "Enums/DebugColor";
export class MainAgent extends AIController {
@ -85,5 +86,9 @@ export class MainAgent extends AIController {
this.ai.userInstruction = await this.aiPrompt.userInstruction();
this.ai.toolDefinitions = AIFunctionDefinitions.agentDefinitions(allowDestructiveActions, planningMode);
}
protected override setDebugColor(): void {
this.debugService?.setDebugColor(DebugColor.BLUE);
}
}

View file

@ -13,6 +13,7 @@ import { OrchestrationResult } from "Types/OrchestrationResult";
import { AgentType } from "Enums/AgentType";
import { AIFunction, isAIFunction } from "Enums/AIFunction";
import { AIFunctionResponse } from "AIClasses/FunctionDefinitions/AIFunctionResponse";
import { DebugColor } from "Enums/DebugColor";
export class OrchestrationAgent extends AIController {
@ -71,6 +72,12 @@ export class OrchestrationAgent extends AIController {
const orchestrationResult = await this.runOrchestrationAgentLoop(planningConversation, callbacks);
if (orchestrationResult.continue) {
if (orchestrationResult.continueContext && index + 1 < executionPlan.executionSteps.length) {
const nextStep = executionPlan.executionSteps[index + 1];
nextStep.context = nextStep.context
? nextStep.context.concat("\n\n", orchestrationResult.continueContext)
: orchestrationResult.continueContext;
}
continue;
}
if (orchestrationResult.abort) {
@ -144,7 +151,7 @@ export class OrchestrationAgent extends AIController {
{ message: "Step Completed" },
functionCall.toolId
));
orchestrationResult = new OrchestrationResult({ continue: true });
orchestrationResult = new OrchestrationResult({ continue: true, continueContext: parseResult.data.context_for_next_step });
return { shouldExit: true }
}
@ -213,5 +220,9 @@ export class OrchestrationAgent extends AIController {
this.ai.userInstruction = ""; // do not include user instruction for orchestration agent
this.ai.toolDefinitions = AIFunctionDefinitions.orchestrationAgentDefinitions();
}
protected override setDebugColor(): void {
this.debugService?.setDebugColor(DebugColor.ORANGE);
}
}

View file

@ -11,6 +11,7 @@ import { AIFunctionDefinitions } from "AIClasses/FunctionDefinitions/AIFunctionD
import { Copy } from "Enums/Copy";
import { ConversationContent } from "Conversations/ConversationContent";
import { Role } from "Enums/Role";
import { DebugColor } from "Enums/DebugColor";
export class PlanningAgent extends AIController {
@ -107,4 +108,8 @@ export class PlanningAgent extends AIController {
this.ai.toolDefinitions = AIFunctionDefinitions.planningAgentDefinitions();
}
protected override setDebugColor(): void {
this.debugService?.setDebugColor(DebugColor.YELLOW);
}
}

27
Services/DebugService.ts Normal file
View file

@ -0,0 +1,27 @@
import { DebugColor } from "Enums/DebugColor";
declare global {
interface Window {
debugServiceLog: (level: string, message: string) => void;
}
}
export class DebugService {
private debugColor: DebugColor = DebugColor.WHITE;
public constructor() {
window.debugServiceLog = (level: string, message: string) => {
console.log(`%c${level}: ${message}`, `color:${this.debugColor};`);
};
}
public setDebugColor(debugColor: DebugColor): void {
this.debugColor = debugColor;
}
public log(level: string, message: string): void {
window.debugServiceLog(level, message);
}
}

View file

@ -13,6 +13,7 @@ export function RegisterTransient<T>(type: symbol, factory: () => T) {
export function Resolve<T>(type: symbol): T {
const service = services.get(type);
if (!service) {
Exception.throw(`Service not found for type: ${type.description}`);
}
@ -26,6 +27,12 @@ export function Resolve<T>(type: symbol): T {
return service as T;
}
export function TryResolve<T>(type: symbol): T | undefined {
if (services.has(type)) {
return Resolve<T>(type);
}
}
export function DeregisterAllServices() {
services.forEach((service) => {
if (service && typeof service === "object" && "unload" in service) {

View file

@ -2,7 +2,7 @@ import { TAbstractFile, TFile, TFolder } from "obsidian";
import { Resolve } from "./DependencyService";
import { Services } from "./Services";
import type { VaultService } from "./VaultService";
import type { ISearchMatch } from "../Helpers/SearchTypes";
import type { ISearchMatch } from "../Types/SearchTypes";
import { Exception } from "Helpers/Exception";
export class FileSystemService {

View file

@ -38,6 +38,8 @@ import { ClaudeFileService } from "AIClasses/Claude/ClaudeFileService";
import { GeminiFileService } from "AIClasses/Gemini/GeminiFileService";
import { OpenAIFileService } from "AIClasses/OpenAI/OpenAIFileService";
import { MainAgent } from "./AIServices/MainAgent";
import { Environment } from "Enums/Environment";
import { DebugService } from "./DebugService";
export async function RegisterPlugin(plugin: VaultkeeperAIPlugin) {
RegisterSingleton<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin, plugin);
@ -45,6 +47,10 @@ export async function RegisterPlugin(plugin: VaultkeeperAIPlugin) {
}
export function RegisterDependencies() {
if (process.env.NODE_ENV === Environment.DEV) {
RegisterTransient<DebugService | undefined>(Services.DebugService, () => new DebugService());
}
RegisterSingleton<EventService>(Services.EventService, new EventService());
RegisterSingleton<AbortService>(Services.AbortService, new AbortService());
RegisterSingleton<HTMLService>(Services.HTMLService, new HTMLService());

View file

@ -20,6 +20,7 @@ export class Services {
static SanitiserService = Symbol("SanitiserService");
static InputService = Symbol("InputService");
static DiffService = Symbol("DiffService");
static DebugService = Symbol("DebugService");
// stores
static SearchStateStore = Symbol("SearchStateStore");

View file

@ -5,7 +5,7 @@ import type VaultkeeperAIPlugin from "main";
import { Path } from "Enums/Path";
import { pathExtname, randomSample, shuffleArray } from "Helpers/Helpers";
import { StringTools } from "Helpers/StringTools";
import type { IPageText, ISearchMatch, ISearchSnippet } from "../Helpers/SearchTypes";
import type { IPageText, ISearchMatch, ISearchSnippet } from "../Types/SearchTypes";
import type { SanitiserService } from "./SanitiserService";
import { FileEvent } from "Enums/FileEvent";
import type { SettingsService } from "./SettingsService";

View file

@ -1,5 +1,6 @@
type OrchestrationResultInit = {
continue?: boolean;
continueContext?: string;
abort?: boolean;
abortContext?: string;
replan?: boolean;
@ -9,6 +10,7 @@ type OrchestrationResultInit = {
export class OrchestrationResult {
public continue: boolean;
public continueContext: string;
public abort: boolean;
public abortContext: string;
public replan: boolean;
@ -16,6 +18,7 @@ export class OrchestrationResult {
constructor(init: OrchestrationResultInit) {
this.continue = init.continue ?? false;
this.continueContext = init.continueContext ?? "";
this.abort = init.abort ?? false;
this.abortContext = init.abortContext ?? "";
this.replan = init.replan ?? false;

View file

@ -4,7 +4,7 @@ import { VaultService } from '../../Services/VaultService';
import { RegisterSingleton, DeregisterAllServices } from '../../Services/DependencyService';
import { Services } from '../../Services/Services';
import { TFile, TFolder, TAbstractFile } from 'obsidian';
import type { ISearchMatch } from '../../Helpers/SearchTypes';
import type { ISearchMatch } from '../../Types/SearchTypes';
import { Exception } from '../../Helpers/Exception';
// Helper function to create mock TFile

View file

@ -8,7 +8,7 @@ import { SettingsService, type IVaultkeeperAISettings } from '../../Services/Set
import { AIProviderModel } from '../../Enums/ApiProvider';
import { Exception } from '../../Helpers/Exception';
import * as PDFHelper from '../../Helpers/PDFHelper';
import type { IPageText } from '../../Helpers/SearchTypes';
import type { IPageText } from '../../Types/SearchTypes';
/**
* PDF-SPECIFIC TESTS FOR VAULTSERVICE

714
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -23,39 +23,39 @@
"@eslint/js": "^9.39.2",
"@testing-library/svelte": "^5.3.1",
"@types/express": "^5.0.6",
"@types/node": "^25.0.3",
"@types/node": "^25.0.10",
"@types/path-browserify": "^1.0.3",
"@typescript-eslint/eslint-plugin": "8.52.0",
"@typescript-eslint/parser": "8.52.0",
"@vitest/ui": "^4.0.16",
"@typescript-eslint/eslint-plugin": "8.54.0",
"@typescript-eslint/parser": "8.54.0",
"@vitest/ui": "^4.0.18",
"builtin-modules": "5.0.0",
"esbuild": "^0.27.2",
"esbuild-svelte": "^0.9.4",
"eslint": "^9.39.2",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-obsidianmd": "^0.1.9",
"happy-dom": "^20.1.0",
"happy-dom": "^20.3.9",
"obsidian": "latest",
"svelte": "^5.46.1",
"svelte": "^5.48.4",
"svelte-check": "^4.3.5",
"svelte-preprocess": "^6.0.3",
"tslib": "2.8.1",
"typescript": "^5.9.3",
"vitest": "^4.0.16"
"vitest": "^4.0.18"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.71.2",
"@google/genai": "^1.34.0",
"@google/genai": "^1.38.0",
"@shikijs/rehype": "^3.21.0",
"core-js": "^3.47.0",
"diff": "^8.0.2",
"core-js": "^3.48.0",
"diff": "^8.0.3",
"diff2html": "^3.4.55",
"express": "^5.2.1",
"fuzzysort": "^3.1.0",
"highlight.js": "^11.11.1",
"katex": "^0.16.27",
"katex": "^0.16.28",
"lowlight": "^3.3.0",
"openai": "^6.15.0",
"openai": "^6.16.0",
"path-browserify": "^1.0.1",
"regex-parser": "^2.3.1",
"rehype-highlight": "^7.0.2",
@ -70,6 +70,6 @@
"unified": "^11.0.5",
"unpdf": "^1.4.0",
"uuid": "^13.0.0",
"zod": "^4.3.5"
"zod": "^4.3.6"
}
}