mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 16:30:27 +00:00
Restructure the AI workflow from a single-agent model to a specialized multi-agent system with distinct roles: - Add AgentType enum (Main, Orchestration, Planning, Execution) to define agent specializations - Replace AIControllerService and AIFunctionService with modular agent classes in Services/AIServices/: - MainAgent: Handles user interaction and delegates to orchestration - OrchestrationAgent: Coordinates plan execution and step-by-step workflow - PlanningAgent: Creates and revises execution plans - ExecutionAgent: Executes individual plan steps - AIController: Base class providing common agent loop functionality - Create specialized prompts (OrchestrationPrompt, ExecutionPrompt) for agent-specific behavior - Add OrchestrationResult type to communicate workflow control decisions (continue, abort, replan) - Introduce agent-specific function scoping: - ExecuteWorkflow for main agent - CompleteTask for execution agent - CompleteStep/Replan/CancelPlan for orchestration agent - SubmitPlan/AskUserQuestionPlanning for planning agent - Update BaseAIClass to use agentType property instead of isPlanningAgent boolean for model selection - Simplify ExecutionPlan and ExecutionStep types by removing execution state tracking (moved to agent coordination) - Remove PlanningEnabledAppendix and ExecutionStatus enum (superseded by agent architecture) - Add comprehensive integration tests for agent workflows This architecture provides better separation of concerns, clearer agent responsibilities, and more robust plan execution with explicit orchestration control flow.
236 lines
No EOL
7.4 KiB
Svelte
236 lines
No EOL
7.4 KiB
Svelte
<script lang="ts">
|
|
import { setElementIcon } from "Helpers/ElementHelper";
|
|
import Spinner from "./Spinner.svelte";
|
|
import { tick } from "svelte";
|
|
import { fade, slide } from "svelte/transition";
|
|
import type { IExecutionPlanState } from "Stores/ExecutionPlanStore";
|
|
import type { Writable } from "svelte/store";
|
|
|
|
export let executionPlanState: Writable<IExecutionPlanState>;
|
|
export let editModeActive = false;
|
|
export let busyPlanning = false;
|
|
|
|
let expanded = false;
|
|
let expandedHeight = 0;
|
|
let collapsedHeight = 0;
|
|
|
|
let wrapperDiv: HTMLDivElement;
|
|
let contentDiv: HTMLDivElement;
|
|
let stepElements: (HTMLDivElement | null)[] = [];
|
|
let isTransitioning = false;
|
|
|
|
$: steps = $executionPlanState.plan?.executionSteps;
|
|
$: activeStepIndex = $executionPlanState.currentStepIndex;
|
|
|
|
$: if (steps) {
|
|
tick().then(updateHeight);
|
|
setTimeout(updateHeight, 500);
|
|
}
|
|
|
|
$: if (activeStepIndex >= 0 && !isTransitioning) {
|
|
scrollToActiveStep();
|
|
}
|
|
|
|
$: if (wrapperDiv && expanded !== undefined) {
|
|
handleExpandTransition();
|
|
}
|
|
|
|
function handleExpandTransition() {
|
|
isTransitioning = true;
|
|
const onTransitionEnd = () => {
|
|
isTransitioning = false;
|
|
wrapperDiv.removeEventListener('transitionend', onTransitionEnd);
|
|
scrollToActiveStep();
|
|
};
|
|
wrapperDiv.addEventListener('transitionend', onTransitionEnd);
|
|
}
|
|
|
|
async function updateHeight() {
|
|
const firstStep = stepElements[0];
|
|
if (!contentDiv || !firstStep) {
|
|
return;
|
|
}
|
|
|
|
expandedHeight = contentDiv.scrollHeight;
|
|
|
|
// Calculate heights fresh each time to ensure accurate measurements
|
|
const stepHeight = firstStep.offsetHeight;
|
|
const separatorHeight = firstStep.nextElementSibling?.clientHeight ?? 0;
|
|
|
|
const stepsToShow = Math.min(3, stepElements.length);
|
|
collapsedHeight = (stepsToShow * stepHeight) + (Math.max(0, stepsToShow - 1) * separatorHeight);
|
|
}
|
|
|
|
async function scrollToActiveStep() {
|
|
if (!wrapperDiv || !$executionPlanState.plan || activeStepIndex === -1) return;
|
|
|
|
await tick();
|
|
const stepElement = stepElements[activeStepIndex];
|
|
if (!stepElement) return;
|
|
|
|
const stepTop = stepElement.offsetTop;
|
|
const stepHeight = stepElement.offsetHeight;
|
|
const wrapperHeight = wrapperDiv.clientHeight;
|
|
const scrollTo = stepTop - (wrapperHeight / 2) + (stepHeight / 2);
|
|
|
|
wrapperDiv.scrollTo({ top: scrollTo, behavior: 'smooth' });
|
|
}
|
|
|
|
function toggleExpand() {
|
|
expanded = !expanded;
|
|
}
|
|
</script>
|
|
|
|
{#if busyPlanning}
|
|
<div id="chat-planning-in-progress" transition:slide>
|
|
<Spinner {editModeActive}/>
|
|
<span id="chat-planning-in-progress-text">{$executionPlanState.plan?.isReplan ? "Replanning in progress..." : "Planning in progress..."}</span>
|
|
</div>
|
|
{/if}
|
|
{#if steps && steps?.length > 0}
|
|
<div id="chat-plan-area"
|
|
out:slide
|
|
on:click={toggleExpand}
|
|
on:keydown={(e) => e.key === 'Enter' && toggleExpand()}
|
|
aria-label={expanded ? "Collapse planned steps" : "Expand planned steps"}
|
|
role="button"
|
|
tabindex=0>
|
|
<div id="chat-plan-steps-wrapper" style:height="{expanded ? expandedHeight : collapsedHeight}px" bind:this={wrapperDiv}>
|
|
<div id="chat-plan-steps" bind:this={contentDiv}>
|
|
{#each steps as step, index }
|
|
<div class="chat-plan-step" bind:this={stepElements[index]}>
|
|
{#if index < activeStepIndex}
|
|
<div class="chat-plan-step-icon" use:setElementIcon={"circle-check"} style:color="var(--color-green)"></div>
|
|
{/if}
|
|
{#if index === activeStepIndex}
|
|
<div class="chat-plan-step-icon">
|
|
<Spinner {editModeActive}/>
|
|
</div>
|
|
{/if}
|
|
{#if index > activeStepIndex}
|
|
<div class="chat-plan-step-icon" use:setElementIcon={"circle"} style:opacity={0.5}></div>
|
|
{/if}
|
|
<span class="chat-plan-step-span">
|
|
{`${index + 1}. ${step.description}`}
|
|
</span>
|
|
</div>
|
|
{#if index < steps.length - 1}
|
|
<div class="chat-plan-step-icon"
|
|
use:setElementIcon={"ellipsis-vertical"}
|
|
style:opacity={index < activeStepIndex ? 1 : 0.25}>
|
|
</div>
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{#if steps.length > 2}
|
|
<div class="chat-plan-fade top-fade" transition:fade></div>
|
|
<div class="chat-plan-fade bottom-fade" transition:fade></div>
|
|
{/if}
|
|
<div id="chat-plan-chevron"
|
|
class="transparent-button"
|
|
use:setElementIcon={expanded ? "chevrons-down-up" : "chevrons-up-down"}>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<style>
|
|
#chat-planning-in-progress {
|
|
display: flex;
|
|
gap: var(--size-4-2);
|
|
background-color: var(--background-secondary-alt);
|
|
border-radius: var(--radius-m);
|
|
padding: var(--size-4-2);
|
|
font-style: italic;
|
|
font-size: var(--font-smaller);
|
|
justify-content: center;
|
|
}
|
|
|
|
#chat-planning-in-progress-text {
|
|
max-width: 90%;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
#chat-plan-area {
|
|
grid-row: 1;
|
|
grid-column: 1;
|
|
display: grid;
|
|
grid-template-rows: auto;
|
|
grid-template-columns: 1fr auto;
|
|
background-color: var(--background-secondary-alt);
|
|
border-radius: var(--radius-m);
|
|
padding: var(--size-4-1) var(--size-4-2);
|
|
cursor: pointer;
|
|
position: relative;
|
|
}
|
|
|
|
#chat-plan-steps-wrapper {
|
|
grid-row: auto;
|
|
grid-column: 1;
|
|
transition: height 0.2s ease-out;
|
|
overflow-x: hidden;
|
|
overflow-y: auto;
|
|
scroll-behavior: smooth;
|
|
max-height: 20vh;
|
|
}
|
|
|
|
#chat-plan-steps-wrapper::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
#chat-plan-steps {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
font-size: var(--font-smallest);
|
|
width: 100%;
|
|
}
|
|
|
|
.chat-plan-step {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.chat-plan-step-span {
|
|
max-width: 90%;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.chat-plan-step-icon {
|
|
display: flex;
|
|
align-items: center;
|
|
padding-right: var(--size-4-2);
|
|
}
|
|
|
|
#chat-plan-chevron {
|
|
grid-row: auto;
|
|
grid-column: 2;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.chat-plan-fade {
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
height: var(--size-4-1);
|
|
border-radius: var(--radius-m);
|
|
pointer-events: none;
|
|
z-index: 1;
|
|
}
|
|
|
|
.top-fade {
|
|
top: var(--size-4-1);
|
|
background-image: linear-gradient(to bottom, var(--background-secondary-alt), transparent);
|
|
}
|
|
|
|
.bottom-fade {
|
|
bottom: var(--size-4-1);
|
|
background-image: linear-gradient(to top, var(--background-secondary-alt), transparent);
|
|
}
|
|
</style> |