diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md deleted file mode 100644 index 79ee931694..0000000000 --- a/docs/GETTING_STARTED.md +++ /dev/null @@ -1,455 +0,0 @@ -# Getting Started with Kandev Backend Development - -> **Note:** This guide reflects the current unified binary implementation. -> The original microservices architecture is documented in IMPLEMENTATION_PLAN.md for future reference. - -## Prerequisites - -### Required Software - -1. **Go 1.21 or higher** - ```bash - # Check version - go version - - # Install from https://go.dev/dl/ - ``` - -2. **Docker** - ```bash - # Check version - docker --version - - # Install from https://docs.docker.com/get-docker/ - ``` - -3. **Augment CLI Credentials** - ```bash - # Verify Augment session exists - cat ~/.augment/session.json - ``` - -4. **Git with SSH Keys Configured** (for repository access) - ```bash - git --version - - # Verify SSH keys exist - ls -la ~/.ssh/ - ``` - ---- - -## Quick Start - -### Step 1: Build the Backend - -```bash -cd apps/backend - -# Build the binary -make build - -# This creates ./bin/kandev -``` - -### Step 2: Run the Server - -```bash -# Run with default settings (creates kandev.db in current directory) -./bin/kandev - -# Or specify a custom database path -KANDEV_DB_PATH=/path/to/kandev.db ./bin/kandev -``` - -The server starts on port 8080 by default. - -### Step 3: Verify It's Running - -The backend uses a WebSocket-first architecture. Connect to `ws://localhost:8080/ws` to interact with the API. - -#### Using websocat (Command Line) - -```bash -# Install websocat -# macOS: brew install websocat -# Linux: cargo install websocat - -# Health check -echo '{"id":"1","type":"request","action":"health.check","payload":{}}' | websocat ws://localhost:8080/ws - -# Should return a response with status "ok" -``` - -#### Using JavaScript - -```javascript -const ws = new WebSocket('ws://localhost:8080/ws'); - -ws.onopen = () => { - ws.send(JSON.stringify({ - id: '1', - type: 'request', - action: 'health.check', - payload: {} - })); -}; - -ws.onmessage = (event) => { - const msg = JSON.parse(event.data); - console.log('Health:', msg); // { id: '1', type: 'response', action: 'health.check', payload: { status: 'ok' }, ... } -}; -``` - ---- - -## WebSocket Message Protocol - -### Connection - -Connect to: `ws://localhost:8080/ws` - -### Message Envelope - -All messages use this envelope format: - -```json -{ - "id": "correlation-uuid", - "type": "request|response|notification|error", - "action": "action.name", - "payload": {}, - "timestamp": "2026-01-10T10:30:00Z" -} -``` - -### Message Types - -- `request` - Client request to the server -- `response` - Server response to a request -- `notification` - Server push notification (e.g., agent status updates) -- `error` - Error response - ---- - -## Basic Usage - -### Create a Board - -#### websocat - -```bash -echo '{"id":"1","type":"request","action":"board.create","payload":{"name":"My Project","description":"Project tasks"}}' | websocat ws://localhost:8080/ws -``` - -#### JavaScript - -```javascript -ws.send(JSON.stringify({ - id: '1', - type: 'request', - action: 'board.create', - payload: { name: 'My Project', description: 'Project tasks' } -})); -``` - -### List Boards - -#### websocat - -```bash -echo '{"id":"2","type":"request","action":"board.list","payload":{}}' | websocat ws://localhost:8080/ws -``` - -#### JavaScript - -```javascript -ws.send(JSON.stringify({ - id: '2', - type: 'request', - action: 'board.list', - payload: {} -})); -``` - -### Create a Column - -#### websocat - -```bash -echo '{"id":"3","type":"request","action":"column.create","payload":{"board_id":"","name":"To Do"}}' | websocat ws://localhost:8080/ws -``` - -#### JavaScript - -```javascript -ws.send(JSON.stringify({ - id: '3', - type: 'request', - action: 'column.create', - payload: { board_id: '', name: 'To Do' } -})); -``` - -### Create a Task - -#### websocat - -```bash -echo '{"id":"4","type":"request","action":"task.create","payload":{"board_id":"","column_id":"","title":"Fix login bug","description":"Users cannot login"}}' | websocat ws://localhost:8080/ws -``` - -#### JavaScript - -```javascript -ws.send(JSON.stringify({ - id: '4', - type: 'request', - action: 'task.create', - payload: { - board_id: '', - column_id: '', - title: 'Fix login bug', - description: 'Users cannot login' - } -})); -``` - -### Launch an Agent for a Task - -#### websocat - -```bash -echo '{"id":"5","type":"request","action":"agent.launch","payload":{"task_id":"","agent_type":"augment-agent","repository_url":"/path/to/your/project"}}' | websocat ws://localhost:8080/ws -``` - -#### JavaScript - -```javascript -ws.send(JSON.stringify({ - id: '5', - type: 'request', - action: 'agent.launch', - payload: { - task_id: '', - agent_type: 'augment-agent', - repository_url: '/path/to/your/project' - } -})); -``` - -### Start Task via Orchestrator - -#### websocat - -```bash -echo '{"id":"6","type":"request","action":"orchestrator.start","payload":{"task_id":""}}' | websocat ws://localhost:8080/ws -``` - -#### JavaScript - -```javascript -ws.send(JSON.stringify({ - id: '6', - type: 'request', - action: 'orchestrator.start', - payload: { task_id: '' } -})); -``` - -### Check Agent Status - -#### websocat - -```bash -echo '{"id":"7","type":"request","action":"agent.status","payload":{"agent_id":""}}' | websocat ws://localhost:8080/ws -``` - -#### JavaScript - -```javascript -ws.send(JSON.stringify({ - id: '7', - type: 'request', - action: 'agent.status', - payload: { agent_id: '' } -})); -``` - -### Interactive Mode - -For interactive testing, connect in interactive mode: - -```bash -websocat ws://localhost:8080/ws -# Then type messages manually, one per line: -{"id":"1","type":"request","action":"health.check","payload":{}} -{"id":"2","type":"request","action":"board.list","payload":{}} -``` - ---- - -## Running the End-to-End Test - -The project includes a comprehensive E2E test script: - -```bash -# Build the agent Docker image first -cd apps/backend/dockerfiles/augment-agent -docker build -t kandev/augment-agent:latest . - -# Run the E2E test -cd /path/to/kandev -./scripts/e2e-test.sh -``` - -The E2E test uses WebSocket connections to: -1. Start the server with a fresh database -2. Connect via WebSocket to `ws://localhost:8080/ws` -3. Create a board, column, and task using WebSocket messages -4. Start the task via the orchestrator (`orchestrator.start`) -5. Verify the agent container starts -6. Check that ACP session is created -7. Receive real-time agent status notifications -8. Wait for agent to complete - ---- - -## Configuration - -| Environment Variable | Default | Description | -|---------------------|---------|-------------| -| `KANDEV_DB_PATH` | `./kandev.db` | SQLite database file path | -| `PORT` | `8080` | HTTP server port | - ---- - -## Development Workflow - -### Build - -```bash -cd apps/backend -make build -``` - -### Run Tests - -```bash -cd apps/backend -make test - -# Or with verbose output -go test -v ./... -``` - -### Code Formatting - -```bash -go fmt ./... -go vet ./... -``` - -### View Database - -```bash -# SQLite CLI -sqlite3 apps/backend/kandev.db - -# List tables -.tables - -# View boards -SELECT * FROM boards; - -# View tasks -SELECT * FROM tasks; -``` - ---- - -## Project Structure - -``` -kandev/ -├── apps/ -│ ├── backend/ # Go backend services -│ │ ├── cmd/kandev/ # Main entry point -│ │ │ └── main.go -│ │ ├── internal/ -│ │ │ ├── agent/ # Agent management -│ │ │ │ ├── acp/ # ACP session management -│ │ │ │ ├── api/ # Agent HTTP handlers -│ │ │ │ ├── credentials/ # Credential providers -│ │ │ │ ├── docker/ # Docker client -│ │ │ │ ├── lifecycle/# Container lifecycle -│ │ │ │ └── registry/ # Agent type registry -│ │ │ ├── task/ # Task management -│ │ │ │ ├── api/ # Task HTTP handlers -│ │ │ │ ├── models/ # Data models -│ │ │ │ ├── repository/ # SQLite/Memory storage -│ │ │ │ └── service/ # Business logic -│ │ │ ├── orchestrator/ # Orchestration logic -│ │ │ ├── events/ # In-memory event bus -│ │ │ └── common/ # Shared utilities -│ │ │ └── logger/ # Zap logger -│ │ ├── pkg/acp/ # ACP protocol types -│ │ │ └── jsonrpc/ # JSON-RPC 2.0 client -│ │ ├── dockerfiles/ # Agent Dockerfiles -│ │ │ └── augment-agent/ -│ │ ├── bin/ # Built binaries (gitignored) -│ │ └── Makefile -│ └── web/ # Frontend application -├── scripts/ -│ └── e2e-test.sh # End-to-end test script -└── docs/ # Documentation -``` - ---- - -## Troubleshooting - -### Docker Permission Issues - -```bash -# Add user to docker group (Linux) -sudo usermod -aG docker $USER -newgrp docker -``` - -### Port Already in Use - -```bash -# Find process using port 8080 -lsof -i :8080 - -# Kill it -kill -9 -``` - -### Database Issues - -```bash -# Delete and recreate database -rm apps/backend/kandev.db -./bin/kandev # Will create fresh database -``` - -### Agent Container Issues - -```bash -# List running containers -docker ps -a --filter "label=kandev.managed=true" - -# View container logs -docker logs - -# Clean up old containers -docker rm $(docker ps -a -q --filter "label=kandev.managed=true") -``` - ---- - -## Next Steps - -See [NEXT_STEPS.md](../apps/backend/NEXT_STEPS.md) for the development roadmap and immediate next steps. diff --git a/docs/agent-orchestration-ui.md b/docs/agent-orchestration-ui.md deleted file mode 100644 index 994da89ba9..0000000000 --- a/docs/agent-orchestration-ui.md +++ /dev/null @@ -1,249 +0,0 @@ -# Agent Orchestration + Session UI Data Flow - -This document describes how a task session is created, how agentctl/agent lifecycle events flow through the backend, and how the session details page consumes those events. - -## Overview (Actors) - -- **Client UI**: Next.js app with session details page and panels (chat, terminal, git, files). -- **Backend**: Orchestrator + WebSocket gateway + task/session store. -- **agentctl**: Sidecar HTTP/WS server inside agent container (or standalone process) that manages the agent subprocess and exposes ACP + workspace streams. -- **Agent**: ACP-speaking process (JSON-RPC over stdin/stdout) that performs work. - -## Orchestration Flow (Task Session Creation) - -``` -Client UI - | HTTP: POST /api/v1/tasks { start_agent: true, agent_profile_id } - v -Backend Task Service - | create task + first session - | start agentctl orchestration - | create task_session (STARTING) - | launch agentctl (container or standalone) - | wait for /health - | POST /api/v1/start - | POST /api/v1/acp/call initialize - | POST /api/v1/acp/call session/new - | POST /api/v1/acp/call session/prompt - v -agentctl - | spawns agent process - | streams ACP updates + workspace streams - v -Agent - | ACP notifications (session/update) - v -agentctl -> Backend - | ACP update stream -> backend - | backend updates task_session state + messages - | backend publishes WS notifications - v -Client UI - | session.subscribe - | receive session-scoped events -``` - -## Backend Event Routing (Session Scoped) - -Events are now routed primarily by **session_id**. The WS gateway maintains **session subscriber lists**. It broadcasts session events only to clients subscribed to that session. - -``` -agentctl WS /api/v1/acp/stream - -> backend ACP handler - -> task_session state update - -> message persisted - -> WS broadcast: session.channel - -agentctl WS /api/v1/workspace/git-status/stream - -> backend git-status handler - -> attach to session - -> WS broadcast: session.channel - -agentctl WS /api/v1/output/stream - -> backend shell handler - -> session resolve (session_id -> task_id) - -> WS broadcast: session.channel -``` - -Session notifications: -- `session.agentctl_starting` -- `session.agentctl_ready` -- `session.agentctl_error` -- `session.state_changed` -- `session.message.added`, `session.message.updated` -- `session.shell.output`, `session.shell.status` -- `session.git.status` -- `session.workspace.file.changes` - -## Frontend Session Page Flow (SSR + WS) - -``` -Server (SSR) - - fetch session by id - - fetch parent task + workspace context - - hydrate store (tasks, sessions, repos, agent profiles) - -Client (Hydrated) - - read sessionId from URL (/s/:sessionId) - - set activeSessionId in store - - subscribe to session events - - child components read from store + hooks -``` - -### Component Subscriptions - -Each component subscribes to the data it needs via hooks. The **WS client deduplicates subscriptions** so multiple components can safely request the same channel. - -``` -TaskSessionPage (/s/:sessionId) - -> useSession(sessionId) - -> subscribe: session.{sessionId} - - TaskChatPanel - -> useSessionMessages(sessionId) - -> subscribe: session.{sessionId} - - ShellTerminal - -> useSessionAgentctl(sessionId) - -> subscribe: session.{sessionId} - -> useShellOutput(sessionId) - -> subscribe: session.{sessionId} - - TaskChangesPanel (Git) - -> useSessionGitStatus(sessionId) - -> subscribe: session.{sessionId} - - TaskFilesPanel - -> useSessionAgentctl(sessionId) - -> wait for readiness - -> useSessionWorktrees(sessionId) - -> subscribe: session.{sessionId} -``` - -## Event + State Flow Diagram - -``` -Agent - | ACP session/update - v -agentctl - | WS /api/v1/acp/stream - v -Backend ACP handler - | update session state/messages - | emit session WS events - v -WS Gateway (session subscribers) - | broadcast to session.{sessionId} - v -WebSocket Client (browser) - | dedupe subscription - | route to store slice - v -Zustand Store - | sessions, messages, shell, git, worktrees - v -Session UI Panels - | hook selectors - | render -``` - -## Concrete Example: New Session -> Message Appears In Chat - -Scenario: user starts a task session and the agent replies with a completion message that appears in the chat panel. - -### Sequence (Calls + Events) - -1) Client -> Backend (HTTP request) -``` -POST /api/v1/tasks -body: - title: "Example task" - workspace_id: "workspace-1" - repository_id: "repo-9" - agent_profile_id: "profile-abc" - start_agent: true -``` - -2) Backend (internal) -- creates task -- creates first task session -- starts agentctl orchestration for the new session -- returns session_id in the HTTP response - -3) Backend -> agentctl (HTTP) -- `POST /api/v1/start` -- `POST /api/v1/acp/call` initialize -- `POST /api/v1/acp/call` session/new -- `POST /api/v1/acp/call` session/prompt - -4) Backend -> Client (WS notification) -``` -action: session.agentctl_starting -payload: - task_id: "task-123" - session_id: "session-789" -``` - -5) Client subscribes to session -``` -action: session.subscribe -payload: - session_id: "session-789" -``` - -6) agentctl -> Backend (ACP notification) -``` -jsonrpc: "2.0" -method: "session/update" -params: - type: "content" - data: - text: "Working on your request..." - session_id: "session-789" -``` - -7) Backend -> Client (WS notification) -``` -action: session.message.added -payload: - session_id: "session-789" - message: - id: "msg-1" - author_type: "agent" - content: "Working on your request..." -``` - -8) Client store update (message handler) -``` -messages.bySession["session-789"] += message -``` - -9) Chat panel render -``` -TaskChatPanel -> useSessionMessages("session-789") -> messages list -> render -``` - -### Full Event List (Typical Session Start) - -- HTTP request: `POST /api/v1/tasks` with `start_agent: true` -- WS notification: `session.agentctl_starting` -- WS request: `session.subscribe` -- WS notification: `session.agentctl_ready` -- ACP notifications from agent: - - `session/update` (content/toolCall/complete/error) -- WS notifications: - - `session.message.added` - - `session.message.updated` - - `session.state_changed` - - `session.shell.status` / `session.shell.output` (if shell active) - - `session.git.status` (if git watcher active) - - `session.workspace.file.changes` (if watcher active) - -## Notes / Guardrails - -- Session is the source of truth; task id is derived from session when needed. -- No fallback to “active task” or “first message” for session ownership. -- Agentctl readiness is explicit; panels that require a live agent should wait for `agentctl_ready`. -- Store slices are keyed by session id (messages, shell output, git status, worktrees). -- WS subscriptions are **session-scoped** and **deduplicated** at the client level. diff --git a/docs/agent-permission-request.md b/docs/agent-permission-request.md deleted file mode 100644 index acec96a7a2..0000000000 --- a/docs/agent-permission-request.md +++ /dev/null @@ -1,104 +0,0 @@ -# Agent Permission Requests - -This document describes how permission requests flow between the agent, backend, and frontend. - -## Overview -Some agents (e.g., Auggie) require user approval for sensitive tool actions (run command, write file, etc.). The agentctl sidecar streams permission requests to the backend, which then broadcasts them to the UI. The user responds via the UI, and the backend forwards the response back to agentctl. - -## High-Level Flow -``` -Agent (ACP/CLI) - | - | permission_request (tool call needs approval) - v -agentctl (permission stream) - | - | WS /api/v1/acp/permissions/stream - v -Backend (lifecycle manager) - | - | events.ACPMessage (type=permission_request) - v -Backend (WS gateway) - | - | session.permission.requested (WS notification) - v -Frontend (CommandApprovalDialog / tool-call UI) - | - | permission.respond (WS request) - v -Backend (orchestrator) - | - | RespondToPermissionBySessionID - v -agentctl (POST /api/v1/acp/permissions/respond) - | - v -Agent continues (approved/rejected) -``` - -## Backend Components -- **agentctl permission stream** - - agentctl exposes `GET /api/v1/acp/permissions/stream`. - - Streams `PermissionNotification` from the agent process. -- **lifecycle StreamManager** - - Connects to the permission stream. - - Publishes `events.ACPMessage` with `type=permission_request`. -- **orchestrator watcher** - - Parses ACP messages and forwards permission requests to WS clients. -- **gateway websocket** - - Broadcasts `session.permission.requested` to session subscribers. -- **orchestrator handlers** - - Handles `permission.respond` and forwards to agentctl. - -## Frontend Components -- **CommandApprovalDialog** - - Shows permission requests not tied to a visible tool call. -- **ToolCallMessage inline actions** - - Shows approve/reject inline for tool call messages. -- **WebSocket client** - - Subscribes to `session.permission.requested`. - - Sends `permission.respond` with `{ session_id, pending_id, option_id, cancelled }`. - -## Message Shapes - -### Permission Requested (WS notification) -```json -{ - "type": "notification", - "action": "session.permission.requested", - "payload": { - "pending_id": "perm-123", - "session_id": "session-abc", - "task_id": "task-xyz", - "tool_call_id": "tool-456", - "title": "Command Execution Approval", - "options": [ - { "option_id": "allow_once", "name": "Allow once", "kind": "allow_once" }, - { "option_id": "reject_once", "name": "Reject", "kind": "reject_once" } - ], - "action_type": "command", - "action_details": { "command": "rm -rf .", "cwd": "/workspace" }, - "created_at": "2026-01-18T00:00:00Z" - } -} -``` - -### Permission Respond (WS request) -```json -{ - "type": "request", - "action": "permission.respond", - "payload": { - "session_id": "session-abc", - "pending_id": "perm-123", - "option_id": "allow_once", - "cancelled": false - } -} -``` - -## Notes -- Permissions are scoped to a **session**, not a task. -- If the session has no active UI, the pending permission is still stored on the backend and sent when the client subscribes. -- The agent can define different tools that require permission via its registry config. diff --git a/docs/agentctl-lifecycle.md b/docs/agentctl-lifecycle.md deleted file mode 100644 index d683257149..0000000000 --- a/docs/agentctl-lifecycle.md +++ /dev/null @@ -1,127 +0,0 @@ -# Agentctl session lifecycle (Kandev) - -This document explains how **agentctl** fits into the Kandev execution flow, what “ready” means, and which parts of the system depend on agentctl being up. - -## What agentctl is - -`agentctl` is a sidecar process that runs inside each agent container (or standalone runtime). It provides: - -- **HTTP API** for lifecycle operations (`/health`, `/api/v1/start`, `/api/v1/stop`, etc.) -- **ACP bridge** (stdin/stdout JSON‑RPC) to the agent process (Auggie/Codex/etc.) -- **WebSockets** for streaming ACP updates and agent output -- **Shell access** to the workspace (terminal output + input) - -It is the backend’s *control plane* for an agent execution. - -## High‑level flow (runtime‑agnostic) - -``` -User action (start task) - | - v -Kandev orchestrator - | - | 1) Create task session in DB - | 2) Launch agent runtime (Docker or standalone) - v -agentctl process starts - | - | 3) HTTP server comes up (health + control APIs) - v -Backend waits for agentctl readiness - | - | 4) agentctl ready => shell/workspace access available - v -ACP session initialization - | - | 5) initialize -> session/new or session/load - | 6) prompt is sent - v -Agent runs, ACP updates stream - | - | 7) session/update notifications => WS to client - v -Task session state transitions -``` - -## “Ready” concepts (important distinction) - -There are **two different “ready” moments**: - -1) **agentctl ready** - - Meaning: agentctl HTTP server is healthy and can accept requests. - - Practical impact: shell/workspace endpoints become usable. - - Source: `lifecycle.Manager.waitForAgentctlReady`. - - Today: only logged; **not emitted as a WS event**. - -2) **agent ready** (prompt completed) - - Meaning: agent finished the current prompt and is idle/ready for follow‑up. - - Source: `events.AgentReady` (after prompt completion). - - Today: internal event for orchestrator state transitions. - -The frontend should not rely on “agent ready” when it needs shell access; shell access depends on agentctl readiness instead. - -## Current backend signals - -- **agentctl readiness** - - `agentctl is ready` (agentctl client log) - - `agentctl ready - shell/workspace access available` (lifecycle manager log) - -- **agent ready** (post‑prompt) - - `events.AgentReady` internal event (used by orchestrator state transitions) - -There is **no WS event** that tells the client “agentctl is ready.” - -## Where shell streaming depends on agentctl - -- Client calls `shell.subscribe` (WS request) -- Backend checks: - - agent execution exists for the task - - agentctl is connected -- Backend starts a streaming loop that relays agentctl shell output as `shell.output` WS notifications - -If agentctl isn’t ready yet, `shell.subscribe` fails and the frontend currently retries. - -## Suggested improvement (optional) - -Introduce a WS notification like: - -``` -{ "action": "task.session.agentctl_ready", "payload": { "task_id": "...", "task_session_id": "..." } } -``` - -So the client can subscribe once and only request shell after it receives readiness. - -## End‑to‑end diagram (detailed) - -``` -Client Backend (Kandev) agentctl Agent - | | | | - | orchestrator.start | | | - |------------------------------->| | | - | | create task session | | - | | launch runtime | | - | |---------------------------->| process starts | - | | | HTTP server up | - | | waitForAgentctlReady | | - | |---------------------------->| /health | - | |<----------------------------| 200 OK | - | | (agentctl ready) | | - | | ACP initialize | | - | |---------------------------->| initialize | - | | ACP session/new | | - | |---------------------------->| session/new | - | | ACP prompt | | - | |---------------------------->| session/prompt | - | | |--------------------->| - | | | agent runs | - | | WS stream | | - |<-------------------------------| (session/update notifications) | -``` - -## References - -- `apps/backend/internal/agent/lifecycle/manager.go` (waitForAgentctlReady) -- `apps/backend/internal/agentctl/client/client.go` (WaitForReady) -- `apps/backend/internal/agent/handlers/shell_handlers.go` (shell.subscribe) -- `apps/backend/internal/events/types.go` (`AgentReady`) diff --git a/docs/architecture/mcp-agentctl-migration.md b/docs/architecture/mcp-agentctl-migration.md deleted file mode 100644 index b9725157ae..0000000000 --- a/docs/architecture/mcp-agentctl-migration.md +++ /dev/null @@ -1,326 +0,0 @@ -# MCP Server Migration: Backend → agentctl - -This document describes the architectural change to move the MCP server from the Kandev backend to agentctl, enabling remote agents to access MCP tools. - -## Problem Statement - -Currently, the MCP server runs embedded in the Kandev backend on the user's machine. When agents run remotely (Docker containers, VPS, Kubernetes), they cannot access the MCP server because it's not reachable from their network location. - -## Solution - -Move the MCP server into agentctl, which always runs co-located with the agent. API calls from MCP tools are tunneled back to the Kandev backend through the existing WebSocket connection. - ---- - -## Current Architecture (MCP Server with Backend) - -```mermaid -flowchart TB - subgraph UserMachine["👤 USER'S MACHINE"] - subgraph Backend["KANDEV BACKEND"] - Orchestrator["Orchestrator"] - Lifecycle["Lifecycle Manager"] - Runtime["Runtime
(Docker/Standalone)"] - - subgraph MCPServer["EMBEDDED MCP SERVER :9090"] - SSE["/sse endpoint"] - MCP["/mcp endpoint"] - Tools["Tools:
list_workspaces
list_tasks
create_task
update_task"] - end - - API["REST API :8080
/api/v1/tasks
/api/v1/boards"] - - Orchestrator --> Lifecycle - Lifecycle --> Runtime - MCPServer -->|"HTTP calls"| API - end - end - - subgraph Remote["🖥️ REMOTE SERVER / CONTAINER"] - subgraph Agentctl["AGENTCTL"] - InstanceMgr["Instance Manager
Ports: 10000+"] - Agent["Agent Process
(Claude, Codex, etc)
stdin/stdout ACP"] - - InstanceMgr --> Agent - end - - NoMCP["🚫 NO MCP ACCESS
Port 9090 unreachable"] - Agent -.->|"❌ CANNOT CONNECT"| NoMCP - end - - Runtime -->|"WS connection"| Agentctl - NoMCP -.->|"❌ Network barrier"| MCPServer - - style NoMCP fill:#ffcccc,stroke:#cc0000 - style MCPServer fill:#ffffcc,stroke:#cccc00 -``` - ---- - -## Proposed Architecture (MCP Server in agentctl) - -```mermaid -flowchart TB - subgraph UserMachine["👤 USER'S MACHINE"] - subgraph Backend["KANDEV BACKEND"] - Orchestrator["Orchestrator"] - Lifecycle["Lifecycle Manager"] - Runtime["Runtime
(Docker/Standalone)"] - - subgraph WSGateway["WS Gateway :8080"] - Dispatcher["Dispatcher"] - MCPHandlers["MCP Handlers"] - end - - Services["Service Layer
(Task, Board, Workspace)"] - - Orchestrator --> Lifecycle - Lifecycle --> Runtime - Dispatcher --> MCPHandlers - MCPHandlers --> Services - end - end - - subgraph Remote["🖥️ REMOTE SERVER / CONTAINER"] - subgraph Agentctl["AGENTCTL"] - InstanceMgr["Instance Manager
Ports: 10000+"] - - subgraph MCPServer["EMBEDDED MCP SERVER"] - SSE["/sse endpoint"] - MCP["/mcp endpoint"] - Tools["Tools:
list_workspaces
list_tasks
create_task
update_task"] - end - - WSTunnel["WS Client
(connects to backend)"] - - Agent["Agent Process
(Claude, Codex, etc)
stdin/stdout ACP"] - - InstanceMgr --> Agent - MCPServer -->|"forward requests"| WSTunnel - Agent -->|"✅ localhost"| MCPServer - end - end - - Runtime <-->|"WS connection"| Agentctl - WSTunnel <-->|"mcp.* actions"| Dispatcher - - style MCPServer fill:#ccffcc,stroke:#00cc00 - style WSTunnel fill:#cce5ff,stroke:#0066cc - style MCPHandlers fill:#ccffcc,stroke:#00cc00 -``` - ---- - -## Sequence Diagram: Agent Using MCP (Proposed) - -```mermaid -sequenceDiagram - participant Agent as Agent Process - participant MCP as MCP Server
(in agentctl) - participant Tunnel as WS Tunnel
(in agentctl) - participant Dispatcher as WS Dispatcher
(in Backend) - participant Handler as MCP Handler
(in Backend) - participant Service as Service Layer
(in Backend) - participant DB as Database - - Note over Agent,DB: Agent wants to create a task - - Agent->>MCP: POST /mcp
tool: create_task - MCP->>MCP: Parse MCP request - - MCP->>Tunnel: Forward request - Tunnel->>Dispatcher: WS message
{action: "mcp.create_task", ...} - Dispatcher->>Handler: Dispatch to registered handler - Handler->>Service: taskService.Create(...) - Service->>DB: Insert task - DB-->>Service: Task created - Service-->>Handler: Task entity - Handler-->>Dispatcher: Response message - Dispatcher-->>Tunnel: WS message
{action: "mcp.create_task", result: {...}} - Tunnel-->>MCP: Response - - MCP-->>Agent: MCP tool result
{task_id: "..."} - - Note over Agent,DB: ✅ Uses existing WS Dispatcher pattern -``` - ---- - -## Component Comparison - -```mermaid -flowchart LR - subgraph Current["❌ CURRENT"] - direction TB - B1["Backend"] - M1["MCP Server"] - A1["agentctl"] - AG1["Agent"] - - B1 --- M1 - B1 -.->|"WS"| A1 - A1 --- AG1 - AG1 -.->|"❌ unreachable"| M1 - end - - subgraph Proposed["✅ PROPOSED"] - direction TB - B2["Backend"] - A2["agentctl"] - M2["MCP Server"] - AG2["Agent"] - - B2 <-->|"WS + Tunnel"| A2 - A2 --- M2 - A2 --- AG2 - AG2 -->|"✅ localhost"| M2 - end - - Current -->|"Migration"| Proposed - - style Current fill:#ffeeee - style Proposed fill:#eeffee -``` - ---- - -## Summary - -| Aspect | Current | Proposed | -|--------|---------|----------| -| **MCP Location** | Backend (user's machine) | agentctl (co-located with agent) | -| **Remote Agent Access** | ❌ Cannot reach MCP | ✅ Always localhost | -| **Network Dependency** | Requires direct network path | Works over existing WS tunnel | -| **Deployment Flexibility** | Limited to local agents | Works anywhere (Docker, VPS, K8s) | -| **API Communication** | MCP → localhost API | MCP → WS Tunnel → Backend API | - ---- - -## Implementation Notes - -### Changes Required - -**agentctl:** -1. Add embedded MCP server (adapt `internal/mcpserver` package) -2. Add WS client that connects back to Kandev backend -3. MCP tool handlers forward requests via WS client instead of HTTP - -**Backend:** -1. Add MCP handlers package (`internal/mcp/handlers/`) -2. Register `mcp.*` actions with WS Dispatcher -3. Handlers call existing services (TaskService, BoardService, etc.) -4. Remove embedded MCP server from `cmd/kandev` - -**Lifecycle Manager:** -1. Pass backend WS URL to agentctl during instance creation -2. agentctl connects back to backend on startup - -### Backend Internal Architecture - -The backend uses the existing **WS Dispatcher pattern** (not the event bus) for MCP requests. This is consistent with how other WS handlers work (tasks, messages, workflows, etc.): - -```mermaid -flowchart TB - subgraph Backend["KANDEV BACKEND"] - subgraph Gateway["WS Gateway"] - Hub["Hub
(client connections)"] - Dispatcher["Dispatcher
(action routing)"] - end - - subgraph Handlers["Registered Handlers"] - TaskH["Task Handlers
task.list, task.create..."] - MsgH["Message Handlers
message.add, message.list..."] - MCPH["MCP Handlers ⭐ NEW
mcp.create_task, mcp.list_tasks..."] - end - - subgraph Services["Service Layer"] - TaskSvc["TaskService"] - BoardSvc["BoardService"] - WorkspaceSvc["WorkspaceService"] - end - - Hub --> Dispatcher - Dispatcher --> TaskH - Dispatcher --> MsgH - Dispatcher --> MCPH - - TaskH --> TaskSvc - MsgH --> TaskSvc - MCPH --> TaskSvc - MCPH --> BoardSvc - MCPH --> WorkspaceSvc - end - - style MCPH fill:#ccffcc,stroke:#00cc00 -``` - -**Why WS Dispatcher instead of Event Bus?** - -| Aspect | WS Dispatcher | Event Bus | -|--------|---------------|-----------| -| **Pattern** | Request/Response | Pub/Sub (fire-and-forget) | -| **Use case** | Synchronous operations | Async notifications | -| **Existing usage** | All WS handlers (tasks, messages, etc.) | Agent lifecycle events, notifications | -| **MCP fit** | ✅ MCP tools need responses | ❌ Would need request-reply wrapper | - -### WS Message Format - -MCP requests use the standard Kandev WS message format with `mcp.*` action prefix: - -```json -{ - "id": "req-123", - "action": "mcp.create_task", - "payload": { - "workspace_id": "ws-1", - "board_id": "board-1", - "title": "New task" - }, - "timestamp": "2024-01-15T10:30:00Z" -} -``` - -Response: - -```json -{ - "id": "req-123", - "action": "mcp.create_task", - "payload": { - "task_id": "task-456", - "title": "New task" - }, - "timestamp": "2024-01-15T10:30:01Z" -} -``` - -Error response: - -```json -{ - "id": "req-123", - "action": "mcp.create_task", - "error": { - "code": "NOT_FOUND", - "message": "Board not found" - }, - "timestamp": "2024-01-15T10:30:01Z" -} -``` - -### MCP Configuration for Agents - -When configuring agents that support MCP, agentctl will provide the local MCP endpoint: - -```json -{ - "mcpServers": { - "kandev": { - "url": "http://localhost:10001/mcp" - } - } -} -``` - -Where `10001` is the instance's allocated port. - diff --git a/docs/architecture/task-specific-executor-refactoring.md b/docs/architecture/task-specific-executor-refactoring.md deleted file mode 100644 index 443aec889b..0000000000 --- a/docs/architecture/task-specific-executor-refactoring.md +++ /dev/null @@ -1,560 +0,0 @@ -# Task-Specific Executor Selection Architecture Refactoring - -> **Status**: Proposal -> **Author**: Generated for architectural review -> **Date**: 2026-01-25 - -## Executive Summary - -This document proposes refactoring the executor selection architecture from a global runtime configuration to a per-task executor selection model. The goal is to enable multiple runtimes (Docker and Standalone) to coexist, with each task independently selecting its execution environment. - ---- - -## 1. Current Architecture - -### 1.1 Global Runtime Configuration - -The backend currently uses a **global runtime configuration** that determines how ALL agents are launched: - -```go -// apps/backend/internal/common/config/config.go -type AgentConfig struct { - Runtime string // "docker" or "standalone" - GLOBAL for all tasks - StandaloneHost string - StandalonePort int -} -``` - -The `provideLifecycleManager()` function in `apps/backend/cmd/kandev/agents.go` creates a **single runtime** based on this global config: - -```go -func provideLifecycleManager(...) (*lifecycle.Manager, *registry.Registry, error) { - var agentRuntime lifecycle.Runtime - switch cfg.Agent.Runtime { - case "standalone": - agentRuntime = lifecycle.NewStandaloneRuntime(...) // ALL tasks use this - default: - agentRuntime = lifecycle.NewDockerRuntime(...) // OR ALL tasks use this - } - // Only ONE runtime is created and passed to the Manager - lifecycleMgr := lifecycle.NewManager(..., agentRuntime, ...) -} -``` - -### 1.2 Why This Is Limiting - -1. **All-or-nothing approach**: Every task uses either Docker OR Standalone, never both -2. **No runtime flexibility**: Can't run lightweight tasks on host while running isolated tasks in Docker -3. **Configuration at startup**: Runtime is fixed when the backend starts; changing requires restart -4. **Underutilized Executor model**: The database has `Executor` with `Type` field (`local_pc`, `local_docker`) but it's not used for runtime selection - -### 1.3 Current Data Flow - -``` -Task Creation - │ - ├── TaskSession.ExecutorID is set (from workspace default) - │ But it's only used for metadata (MCP policy), not runtime selection - │ - └── executor.ExecuteWithProfile() - │ - └── LaunchAgent() → lifecycle.Manager.Launch() - │ - └── m.runtime.CreateInstance() ← Uses GLOBAL runtime -``` - -### 1.4 Existing Executor Infrastructure - -The database already has: -- `executors` table with `type` field (local_pc, local_docker, remote_docker, etc.) -- `task_sessions.executor_id` foreign key -- `executors_running.runtime` field (currently underutilized) -- Default executors seeded: "Local PC" (exec-local-pc) and "Local Docker" (exec-local-docker) - ---- - -## 2. Proposed Architecture - -### 2.1 Core Change: Multi-Runtime Manager - -Replace the single-runtime lifecycle manager with a **runtime registry** that can dispatch to multiple runtimes based on executor type: - -```go -// NEW: RuntimeRegistry manages multiple runtimes -type RuntimeRegistry struct { - runtimes map[runtime.Name]Runtime // docker, standalone, etc. - logger *logger.Logger -} - -func (r *RuntimeRegistry) GetRuntime(name runtime.Name) (Runtime, error) { - rt, ok := r.runtimes[name] - if !ok { - return nil, fmt.Errorf("runtime %q not available", name) - } - return rt, nil -} - -// Lifecycle Manager now uses RuntimeRegistry instead of single Runtime -type Manager struct { - runtimeRegistry *RuntimeRegistry // Replaces: runtime Runtime - // ... rest unchanged -} -``` - -### 2.2 Executor Type → Runtime Mapping - -```go -// ExecutorType to Runtime mapping -func ExecutorTypeToRuntime(execType models.ExecutorType) runtime.Name { - switch execType { - case models.ExecutorTypeLocalPC: - return runtime.NameStandalone - case models.ExecutorTypeLocalDocker: - return runtime.NameDocker - case models.ExecutorTypeRemoteDocker: - return runtime.NameDocker // Future: separate RemoteDockerRuntime - default: - return runtime.NameStandalone // Fallback - } -} -``` - -### 2.3 New Data Flow - -``` -Task Creation (with executor_id) - │ - ├── TaskSession.ExecutorID → Lookup Executor → Get ExecutorType - │ - └── executor.ExecuteWithProfile() - │ - └── LaunchAgentRequest now includes ExecutorType - │ - └── lifecycle.Manager.Launch() - │ - ├── Resolve runtime: ExecutorTypeToRuntime(req.ExecutorType) - │ - └── runtimeRegistry.GetRuntime(runtimeName).CreateInstance() -``` - -### 2.4 Configuration Changes - -**Remove** the global `cfg.Agent.Runtime` setting. Instead: - -```yaml -# BEFORE: Global runtime selection (to be removed) -agent: - runtime: "standalone" # DELETE THIS - -# AFTER: Runtime availability configuration -agent: - docker: - enabled: true - # Docker-specific config (image, network, etc.) - standalone: - host: "localhost" - port: 9999 - # NOTE: No "enabled" flag - agentctl always runs as a core service -``` - -**Important**: The `standalone` section configures connection settings for the agentctl process, but there's no `enabled` flag. The agentctl launcher always starts as a core service - it's the control plane for standalone execution and is always available. - ---- - -## 3. Files Requiring Modification - -### 3.1 Configuration Layer - -| File | Change | -|------|--------| -| `internal/common/config/config.go` | Remove `Runtime` field; add `Docker.Enabled` (standalone is always on) | -| `cmd/kandev/agents.go` | Refactor `provideLifecycleManager()` to create RuntimeRegistry with both runtimes | -| `cmd/kandev/agentctl.go` | **Always start agentctl launcher** (remove conditional check) | - -### 3.2 Lifecycle Manager - -| File | Change | -|------|--------| -| `internal/agent/lifecycle/manager.go` | Replace `runtime Runtime` with `runtimeRegistry *RuntimeRegistry`; update `Launch()` to select runtime via registry | -| `internal/agent/lifecycle/runtime_registry.go` | **NEW FILE**: RuntimeRegistry implementation | -| `internal/agent/lifecycle/runtime.go` | Add `ExecutorTypeToRuntime()` mapping function | - -> **Design Principle**: Keep `manager.go` free of runtime-specific code (no Docker imports, no standalone-specific logic). All runtime-specific behavior should be encapsulated in the `Runtime` interface implementations (`runtime_docker.go`, `runtime_standalone.go`) and dispatched through the `RuntimeRegistry`. The manager should only interact with the abstract `Runtime` interface. - -### 3.3 Orchestrator/Executor - -| File | Change | -|------|--------| -| `internal/orchestrator/executor/executor.go` | Pass `ExecutorType` in `LaunchAgentRequest`; resolve executor before launch | -| `internal/agent/lifecycle/types.go` | Add `ExecutorType` field to `LaunchRequest` | -| `cmd/kandev/adapters.go` | Pass `ExecutorType` from executor request to lifecycle request | - -### 3.4 Data Layer - -| File | Change | -|------|--------| -| `internal/task/models/models.go` | Add helper: `Executor.GetRuntimeName()` | -| `internal/agent/runtime/runtime.go` | Add `ExecutorTypeToRuntime()` function | - -### 3.5 Recovery/Resumption - -| File | Change | -|------|--------| -| `internal/orchestrator/service.go` | `resumeExecutorsOnStartup()` must iterate all available runtimes | -| `internal/agent/lifecycle/manager.go` | `RecoverExecutions()` must query each runtime in registry | - ---- - -## 4. Implementation Plan - -### Phase 1: Infrastructure Setup (Non-Breaking) - -1. **Create RuntimeRegistry** (`runtime_registry.go`) - - Implement registry with `Register()`, `GetRuntime()`, `List()`, `HealthCheckAll()` - - Add `ExecutorTypeToRuntime()` mapping function - -2. **Update Configuration** - - Add `Docker.Enabled` and `Standalone.Enabled` fields - - Keep `Runtime` field temporarily for backward compatibility - - Add deprecation warning if `Runtime` is used - -3. **Refactor `provideLifecycleManager()`** - - Create both runtimes if enabled - - Build RuntimeRegistry with available runtimes - - Pass registry to Manager (but Manager still uses single runtime internally) - -### Phase 2: Lifecycle Manager Refactoring - -4. **Update Manager to use RuntimeRegistry** - - Change `runtime Runtime` to `runtimeRegistry *RuntimeRegistry` - - Add `ExecutorType` to `LaunchRequest` - - Update `Launch()` to: - ```go - runtimeName := ExecutorTypeToRuntime(req.ExecutorType) - rt, err := m.runtimeRegistry.GetRuntime(runtimeName) - if err != nil { - return nil, fmt.Errorf("runtime not available: %w", err) - } - runtimeInstance, err := rt.CreateInstance(ctx, runtimeReq) - ``` - -5. **Update RecoverExecutions()** - - Iterate all registered runtimes - - Merge recovered instances from each runtime - -### Phase 3: Executor Integration - -6. **Update `LaunchAgentRequest`** - - Add `ExecutorType models.ExecutorType` field - -7. **Update `executor.ExecuteWithProfile()`** - - Resolve executor before launch - - Pass executor type in request - -8. **Update `lifecycleAdapter.LaunchAgent()`** - - Map executor type to lifecycle request - -### Phase 4: Cleanup - -9. **Remove global `cfg.Agent.Runtime`** - - Delete from config struct - - Update flag parsing - - Update documentation - -10. **Update agentctl launcher logic** - - **Always start** the agentctl launcher (remove all conditional checks) - - agentctl is a core service, not dependent on runtime configuration - ---- - -## 5. Edge Cases and Challenges - -### 5.1 Session Resumption Across Runtimes - -**Challenge**: A session started on Docker cannot be resumed on Standalone (different environment). - -**Solution**: Store runtime name in `executors_running.runtime` and validate on resume: -```go -func (e *Executor) ResumeSession(...) { - running, _ := e.repo.GetExecutorRunningBySessionID(session.ID) - if running != nil && running.Runtime != "" { - // Use the same runtime that was used originally - req.ExecutorType = RuntimeToExecutorType(running.Runtime) - } -} -``` - -### 5.2 Runtime Availability Changes - -**Challenge**: Task was created with Docker executor, but Docker is disabled on restart. - -**Solution**: -- Check runtime availability before launch -- Return clear error: "Runtime 'docker' is not available. Enable it in configuration or change the task's executor." -- Frontend can show unavailable executors as disabled - -### 5.3 Fallback Behavior - -**Challenge**: Executor not set on task/session. - -**Solution**: Define precedence: -1. Session's executor (if resuming) -2. Workspace default executor -3. System default ("Local PC" → Standalone) - -### 5.4 agentctl Architecture - -**Clarification**: agentctl runs in **both** modes: -- **Docker**: agentctl runs inside each container (started by entrypoint) -- **Standalone**: agentctl runs as a subprocess on the host, managing multiple agent instances - -**Current**: agentctl launcher only started if `cfg.Agent.Runtime == "standalone"` - -**New**: agentctl launcher should **always start** on the host. It serves as the control plane for standalone execution. The launcher startup should not depend on any runtime configuration - it's a core service that's always available. - -### 5.5 Container Recovery - -**Challenge**: Docker runtime recovers containers on startup; Standalone doesn't persist instances. - -**Solution**: -- Docker: `RecoverInstances()` finds containers by label -- Standalone: Returns empty (sessions recreated on resume via `LaunchAgent()`) -- RuntimeRegistry: Iterates all runtimes, merges results - ---- - -## 6. Database Schema Changes - -No schema changes required. Existing fields are sufficient: -- `task_sessions.executor_id` - Links to selected executor -- `executors.type` - Determines runtime (local_pc → standalone, local_docker → docker) -- `executors_running.runtime` - Records which runtime was used - ---- - -## 7. API Changes - -### 7.1 Task Creation (No Change Needed) - -The `executor_id` can already be passed via: -- Workspace default (`workspace.default_executor_id`) -- Potentially extended to task creation payload in future - -### 7.2 New: Runtime Status Endpoint - -```json -GET /api/v1/runtimes/status -{ - "runtimes": [ - { "name": "standalone", "available": true, "healthy": true }, - { "name": "docker", "available": true, "healthy": true } - ] -} -``` - -This allows the frontend to: -- Show which executors are available -- Disable executor selection for unavailable runtimes - ---- - -## 8. Testing Strategy - -1. **Unit Tests** - - `RuntimeRegistry.GetRuntime()` for registered/unregistered runtimes - - `ExecutorTypeToRuntime()` mapping - - `Launch()` with different executor types - -2. **Integration Tests** - - Start task with `local_pc` executor → uses Standalone runtime - - Start task with `local_docker` executor → uses Docker runtime - - Resume session maintains original runtime - - Backend restart with Docker disabled → sessions on Docker can't resume - -3. **E2E Tests** - - Create workspace with Docker as default executor - - Start task → runs in Docker container - - Change workspace default to Local PC - - Start new task → runs via Standalone - - Both tasks run concurrently with different runtimes - ---- - -## 9. Code Examples - -### 9.1 RuntimeRegistry Implementation - -```go -// internal/agent/lifecycle/runtime_registry.go -package lifecycle - -import ( - "context" - "fmt" - "sync" - - "github.com/kandev/kandev/internal/agent/runtime" - "github.com/kandev/kandev/internal/common/logger" -) - -type RuntimeRegistry struct { - runtimes map[runtime.Name]Runtime - mu sync.RWMutex - logger *logger.Logger -} - -func NewRuntimeRegistry(log *logger.Logger) *RuntimeRegistry { - return &RuntimeRegistry{ - runtimes: make(map[runtime.Name]Runtime), - logger: log, - } -} - -func (r *RuntimeRegistry) Register(rt Runtime) { - r.mu.Lock() - defer r.mu.Unlock() - r.runtimes[rt.Name()] = rt - r.logger.Info("registered runtime", zap.String("name", string(rt.Name()))) -} - -func (r *RuntimeRegistry) GetRuntime(name runtime.Name) (Runtime, error) { - r.mu.RLock() - defer r.mu.RUnlock() - rt, ok := r.runtimes[name] - if !ok { - return nil, fmt.Errorf("runtime %q not registered", name) - } - return rt, nil -} - -func (r *RuntimeRegistry) List() []runtime.Name { - r.mu.RLock() - defer r.mu.RUnlock() - names := make([]runtime.Name, 0, len(r.runtimes)) - for name := range r.runtimes { - names = append(names, name) - } - return names -} - -func (r *RuntimeRegistry) HealthCheckAll(ctx context.Context) map[runtime.Name]error { - r.mu.RLock() - defer r.mu.RUnlock() - results := make(map[runtime.Name]error) - for name, rt := range r.runtimes { - results[name] = rt.HealthCheck(ctx) - } - return results -} - -func (r *RuntimeRegistry) RecoverAll(ctx context.Context) ([]*RuntimeInstance, error) { - r.mu.RLock() - defer r.mu.RUnlock() - var allRecovered []*RuntimeInstance - for _, rt := range r.runtimes { - recovered, err := rt.RecoverInstances(ctx) - if err != nil { - r.logger.Warn("failed to recover instances from runtime", - zap.String("runtime", string(rt.Name())), - zap.Error(err)) - continue - } - allRecovered = append(allRecovered, recovered...) - } - return allRecovered, nil -} -``` - -### 9.2 Updated provideLifecycleManager - -```go -// apps/backend/cmd/kandev/agents.go -func provideLifecycleManager( - ctx context.Context, - cfg *config.Config, - log *logger.Logger, - eventBus bus.EventBus, - dockerClient *docker.Client, - agentSettingsRepo settingsstore.Repository, -) (*lifecycle.Manager, *registry.Registry, error) { - - // Create runtime registry - runtimeRegistry := lifecycle.NewRuntimeRegistry(log) - - // Standalone runtime is always available (agentctl is a core service) - controlClient := agentctl.NewControlClient( - cfg.Agent.Standalone.Host, - cfg.Agent.Standalone.Port, - log, - ) - standaloneRuntime := lifecycle.NewStandaloneRuntime( - controlClient, - cfg.Agent.Standalone.Host, - cfg.Agent.Standalone.Port, - log, - ) - runtimeRegistry.Register(standaloneRuntime) - log.Info("Standalone runtime registered") - - // Register Docker runtime if enabled and Docker client is available - if cfg.Agent.Docker.Enabled && dockerClient != nil { - dockerRuntime := lifecycle.NewDockerRuntime(dockerClient, log) - runtimeRegistry.Register(dockerRuntime) - log.Info("Docker runtime registered") - } - - // ... rest of initialization using runtimeRegistry - // NOTE: Manager receives the registry, not individual runtimes - // This keeps manager.go free of runtime-specific code - lifecycleMgr := lifecycle.NewManager( - agentRegistry, eventBus, runtimeRegistry, /* ... */ - ) - return lifecycleMgr, agentRegistry, nil -} -``` - -### 9.3 Updated Launch Method - -```go -// internal/agent/lifecycle/manager.go -func (m *Manager) Launch(ctx context.Context, req *LaunchRequest) (*AgentExecution, error) { - // ... existing validation and setup ... - - // Determine which runtime to use based on executor type - runtimeName := runtime.ExecutorTypeToRuntime(req.ExecutorType) - if runtimeName == runtime.NameUnknown { - runtimeName = runtime.NameStandalone // Default fallback - } - - rt, err := m.runtimeRegistry.GetRuntime(runtimeName) - if err != nil { - return nil, fmt.Errorf("runtime %q not available for executor type %q: %w", - runtimeName, req.ExecutorType, err) - } - - // Create runtime instance using the selected runtime - runtimeInstance, err := rt.CreateInstance(ctx, runtimeReq) - if err != nil { - return nil, fmt.Errorf("failed to create instance: %w", err) - } - - // Store runtime name for session resumption - execution := runtimeInstance.ToAgentExecution(runtimeReq) - execution.RuntimeName = runtimeName // NEW FIELD - - // ... rest unchanged ... -} -``` - ---- - -## 10. Migration Path - -Since breaking changes are acceptable, the migration is straightforward: - -1. **Deploy new backend** with both runtimes enabled -2. **Running sessions continue** (Docker containers persist; Standalone sessions will need resume) -3. **New tasks** use executor-specific runtime selection -4. **Remove old config** after verifying stability - -No database migration needed - schema already supports this model. \ No newline at end of file diff --git a/docs/asyncapi.yaml b/docs/asyncapi.yaml deleted file mode 100644 index 3e83689e9b..0000000000 --- a/docs/asyncapi.yaml +++ /dev/null @@ -1,409 +0,0 @@ -asyncapi: 2.6.0 -info: - title: Kandev WebSocket API - description: | - Kandev is a Kanban board system with AI agent orchestration capabilities. - - ## Overview - - **Task Service**: Manages tasks, boards, and columns - - **Agent Manager**: Manages Docker container lifecycle for AI agents - - **Orchestrator**: Coordinates agent execution - - ## WebSocket Protocol - All communication happens over a single WebSocket connection. - Messages use a JSON envelope format with request/response correlation. - - ## Agent Communication Protocol (ACP) - Agents communicate via JSON messages on stdout. Real-time updates are - pushed to clients via WebSocket notifications. - - ⚠️ **This documentation must be updated when API changes.** - version: 2.0.0 - contact: - name: Kandev Team - license: - name: MIT - -servers: - development: - url: ws://localhost:8080/ws - protocol: ws - description: Local development WebSocket server - -defaultContentType: application/json - -channels: - /: - publish: - summary: Send messages to the server - operationId: sendMessage - message: - oneOf: - - $ref: '#/components/messages/Request' - subscribe: - summary: Receive messages from the server - operationId: receiveMessage - message: - oneOf: - - $ref: '#/components/messages/Response' - - $ref: '#/components/messages/Notification' - - $ref: '#/components/messages/Error' - -components: - messages: - Request: - name: Request - title: Client Request - summary: Request message from client to server - contentType: application/json - payload: - $ref: '#/components/schemas/RequestEnvelope' - - Response: - name: Response - title: Server Response - summary: Response message from server to client - contentType: application/json - payload: - $ref: '#/components/schemas/ResponseEnvelope' - - Notification: - name: Notification - title: Server Notification - summary: Push notification from server to client - contentType: application/json - payload: - $ref: '#/components/schemas/NotificationEnvelope' - - Error: - name: Error - title: Error Response - summary: Error response from server - contentType: application/json - payload: - $ref: '#/components/schemas/ErrorEnvelope' - - schemas: - # Message Envelopes - RequestEnvelope: - type: object - required: [id, type, action, payload] - properties: - id: - type: string - format: uuid - description: Correlation ID for request/response matching - type: - type: string - enum: [request] - action: - type: string - description: Action to perform - enum: - - health.check - - board.list - - board.create - - board.get - - board.update - - board.delete - - column.list - - column.create - - column.get - - task.list - - task.create - - task.get - - task.update - - task.delete - - task.move - - task.state - - task.subscribe - - task.unsubscribe - - agent.list - - agent.launch - - agent.status - - agent.logs - - agent.stop - - agent.prompt - - agent.cancel - - agent.session - - agent.types - - orchestrator.status - - orchestrator.queue - - orchestrator.trigger - - orchestrator.start - - orchestrator.stop - - orchestrator.prompt - - orchestrator.complete - payload: - type: object - description: Action-specific payload - timestamp: - type: string - format: date-time - - ResponseEnvelope: - type: object - required: [id, type, action, payload] - properties: - id: - type: string - format: uuid - description: Correlation ID matching the request - type: - type: string - enum: [response] - action: - type: string - description: Action that was performed - payload: - type: object - description: Response data - timestamp: - type: string - format: date-time - - NotificationEnvelope: - type: object - required: [type, action, payload] - properties: - type: - type: string - enum: [notification] - action: - type: string - description: Notification type - enum: - - acp.progress - - acp.log - - acp.result - - acp.error - - acp.status - - acp.heartbeat - - task.updated - - agent.updated - - agent.available.updated - payload: - type: object - description: Notification data - timestamp: - type: string - format: date-time - - ErrorEnvelope: - type: object - required: [id, type, action, payload] - properties: - id: - type: string - format: uuid - description: Correlation ID matching the request - type: - type: string - enum: [error] - action: - type: string - description: Action that failed - payload: - $ref: '#/components/schemas/ErrorPayload' - timestamp: - type: string - format: date-time - - ErrorPayload: - type: object - required: [code, message] - properties: - code: - type: string - description: Error code - message: - type: string - description: Human readable message - details: - type: object - description: Additional error details - - # Domain Models (keep from openapi.yaml) - Board: - type: object - properties: - id: - type: string - format: uuid - name: - type: string - description: - type: string - created_at: - type: string - format: date-time - updated_at: - type: string - format: date-time - required: [id, name, created_at, updated_at] - - Column: - type: object - properties: - id: - type: string - format: uuid - board_id: - type: string - format: uuid - name: - type: string - position: - type: integer - state: - type: string - enum: [TODO, IN_PROGRESS, DONE] - created_at: - type: string - format: date-time - required: [id, board_id, name, position, created_at] - - Task: - type: object - properties: - id: - type: string - format: uuid - board_id: - type: string - format: uuid - column_id: - type: string - format: uuid - title: - type: string - description: - type: string - state: - type: string - enum: [TODO, IN_PROGRESS, BLOCKED, COMPLETED, FAILED, CANCELLED] - priority: - type: integer - agent_type: - type: string - position: - type: integer - created_at: - type: string - format: date-time - updated_at: - type: string - format: date-time - metadata: - type: object - additionalProperties: true - required: [id, board_id, column_id, title, state, created_at, updated_at] - - AgentInstance: - type: object - properties: - id: - type: string - format: uuid - task_id: - type: string - format: uuid - agent_type: - type: string - container_id: - type: string - status: - type: string - enum: [starting, running, completed, failed, stopped, READY] - progress: - type: integer - minimum: 0 - maximum: 100 - started_at: - type: string - format: date-time - finished_at: - type: string - format: date-time - exit_code: - type: integer - error_message: - type: string - metadata: - type: object - additionalProperties: true - required: [id, task_id, agent_type, status, progress, started_at] - - AgentType: - type: object - properties: - id: - type: string - name: - type: string - description: - type: string - image: - type: string - capabilities: - type: array - items: - type: string - enabled: - type: boolean - required: [id, name, image, enabled] - - # ACP Notification Payloads - ACPProgress: - type: object - properties: - agent_id: - type: string - task_id: - type: string - progress: - type: integer - message: - type: string - current_file: - type: string - files_processed: - type: integer - total_files: - type: integer - - ACPLog: - type: object - properties: - agent_id: - type: string - task_id: - type: string - level: - type: string - enum: [debug, info, warn, error] - message: - type: string - metadata: - type: object - - ACPResult: - type: object - properties: - agent_id: - type: string - task_id: - type: string - status: - type: string - summary: - type: string - artifacts: - type: array - items: - type: object - properties: - type: - type: string - path: - type: string diff --git a/docs/design/dynamic-model-switching.md b/docs/design/dynamic-model-switching.md deleted file mode 100644 index 6ac1f1edde..0000000000 --- a/docs/design/dynamic-model-switching.md +++ /dev/null @@ -1,478 +0,0 @@ -# Dynamic Model Switching Feature Analysis - -> **Status**: Design Document -> **Created**: 2026-01-21 -> **Scope**: Mid-session model switching within the Kandev application - -## Executive Summary - -This document analyzes the implementation of dynamic model switching, allowing users to change the AI model mid-session without starting a new session. The feature requires UI changes to expose model selection, backend logic to detect model changes and restart agents, and careful handling of session continuity. - -## Current Architecture Overview - -### Session Lifecycle (Current Flow) - -``` -User clicks "Start Session" - → Frontend sends orchestrator.start with agentProfileId - → Orchestrator.StartTask → Executor.ExecuteWithProfile - → Lifecycle Manager resolves profile → gets model from AgentProfileInfo - → CommandBuilder.BuildCommand adds --model flag - → Agent process started with fixed model - → ACP session initialized - → User sends prompts via session/prompt -``` - -**Key Insight**: Model is determined at session start via `AgentProfileInfo.Model` and baked into the agent command. There's no current mechanism to change the model mid-session. - -### Relevant Components - -| Component | Path | Role | -|-----------|------|------| -| TaskChatPanel | `apps/web/components/task/task-chat-panel.tsx` | Chat UI, displays current model | -| SessionsDropdown | `apps/web/components/task/sessions-dropdown.tsx` | Session switcher | -| Orchestrator Service | `apps/backend/internal/orchestrator/service.go` | PromptTask, StartTask, ResumeTaskSession | -| Executor | `apps/backend/internal/orchestrator/executor/executor.go` | Agent launch, prompt forwarding | -| Lifecycle Manager | `apps/backend/internal/agent/lifecycle/manager.go` | Agent process management | -| CommandBuilder | `apps/backend/internal/agent/lifecycle/command.go` | Builds CLI args including --model | -| ProfileResolver | `apps/backend/internal/agent/lifecycle/profile_resolver.go` | Resolves profile → model | -| SessionManager | `apps/backend/internal/agent/lifecycle/session.go` | ACP session init/prompt | -| TaskSession Model | `apps/backend/internal/task/models/models.go` | Session persistence | -| Message Service | `apps/backend/internal/task/service/service.go` | CreateMessage | - ---- - -## Requirements Breakdown - -### UI Requirements - -1. **Model Selector in Session View**: Replace existing agent selector with model dropdown -2. **Deferred Application**: Selection does NOT take effect immediately -3. **Apply on Prompt**: Selected model applied when user sends next prompt -4. **Persistent State**: All subsequent prompts use new model until changed again - -### Backend Requirements - -1. **Model Change Detection**: Compare current model vs requested model on prompt -2. **System Message Insertion**: "Model switching to [model name]" message -3. **Agent Restart**: Stop current agent, restart with new `--model` flag -4. **Session Resumption**: Attempt to resume ACP session after restart (if supported) - ---- - -## Technical Approach - -### Phase 1: Frontend Model Selector - -#### 1.1 Store State for Pending Model - -Add to the Zustand store a per-session "pending model" that tracks what model the user wants to switch to. - -**Files to modify:** -- `apps/web/lib/state/store.ts` - Add `pendingModelBySessionId: Record` -- `apps/web/lib/types/http.ts` - Add model-related types if needed - -#### 1.2 Model Selector Component - -Create a model dropdown in the chat panel that: -- Shows available models for the current agent type -- Shows current model (from session's agent profile) -- Allows selection of a different model -- Stores selection in `pendingModelBySessionId[sessionId]` - -**Files to modify:** -- `apps/web/components/task/task-chat-panel.tsx` - Add ModelSelector component -- May create new component: `apps/web/components/task/model-selector.tsx` - -**Data source for available models:** -- From `AvailableAgent.model_config.available_models` in the agent discovery store -- Need to resolve which agent the session is using via `session.agent_profile_id` - -#### 1.3 Prompt Submission with Model Override - -When user submits a prompt, check if `pendingModelBySessionId[sessionId]` differs from current model: -- If different: include `model` field in WebSocket prompt payload -- Clear pending model after submission (backend will handle the switch) - -**WebSocket payload change:** - -```typescript -// Current -{ action: "orchestrator.prompt", payload: { taskId, sessionId, content } } - -// Proposed -{ action: "orchestrator.prompt", payload: { taskId, sessionId, content, model?: string } } -``` - -**Files to modify:** -- `apps/web/lib/ws/actions.ts` - Add `model` to prompt action payload -- `apps/web/components/task/task-chat-panel.tsx` - Include model in prompt submission - ---- - -### Phase 2: Backend Model Change Detection - -#### 2.1 Extend Prompt Handling - -Modify the orchestrator's `PromptTask` to accept an optional model parameter. - -**Files to modify:** -- `apps/backend/internal/orchestrator/service.go` - `PromptTask` method -- `apps/backend/internal/ws/handlers/orchestrator.go` - Parse model from WS payload - -**Logic flow:** -``` -PromptTask(taskId, sessionId, content, model) - → If model != currentExecution.Model: - → Insert system message "Switching to {model}..." - → Stop current agent (graceful shutdown) - → Update session's effective model - → Restart agent with new --model flag - → Resume or reinitialize ACP session - → Forward prompt to agent -``` - -#### 2.2 Executor Model Switch - -The executor needs a new method to handle model switching: - -**Files to modify:** -- `apps/backend/internal/orchestrator/executor/executor.go` - - Add `SwitchModel(sessionId, newModel)` method - - Coordinates stop → rebuild → restart → resume - -**Implementation sketch:** -```go -func (e *Executor) SwitchModel(ctx context.Context, sessionId, newModel string) error { - exec, ok := e.running[sessionId] - if !ok { - return ErrSessionNotFound - } - - // 1. Stop current agent - e.lifecycleManager.StopAgent(ctx, exec.AgentID) - - // 2. Update profile with new model - newProfileInfo := exec.ProfileInfo - newProfileInfo.Model = newModel - - // 3. Restart agent with new command - newAgentID, err := e.lifecycleManager.Launch(ctx, LaunchOpts{ - Model: newModel, - // ... other opts from exec - }) - - // 4. Attempt session resume - e.lifecycleManager.InitializeSession(ctx, newAgentID, InitOpts{ - IsResume: true, - ResumeToken: exec.ResumeToken, - }) - - return nil -} -``` - -#### 2.3 System Message for Model Switch - -Insert a visible message in the chat indicating the model change. - -**Files to modify:** -- `apps/backend/internal/task/service/service.go` - Use `CreateMessage` with type `status` - -**Message format:** -```json -{ - "type": "status", - "content": "Switching model to Claude Opus 4.5...", - "metadata": { - "status_type": "model_switch", - "from_model": "sonnet4.5", - "to_model": "opus4.5" - } -} -``` - ---- - -### Phase 3: Session Resumption After Restart - -#### 3.1 Agent-Specific Resumption - -Different agents handle resumption differently (from `agents.json`): - -| Agent | `resume_via_acp` | `resume_flag` | `can_recover` | -|-------|------------------|---------------|---------------| -| auggie-agent | false | `--resume` | false | -| codex-agent | true | (none) | true | - -**For Codex-style agents (resume_via_acp: true):** -- Use ACP `session/load` with the stored `ResumeToken` -- Session history is preserved by the agent - -**For Auggie-style agents (resume_via_acp: false, can_recover: false):** -- Cannot truly resume; must reinitialize with fresh ACP session -- **Decision**: Accept context loss, display system message: "Model switched. A new session was started." - -#### 3.2 Handling Resume Failures - -**Files to modify:** -- `apps/backend/internal/agent/lifecycle/session.go` - `InitializeSession` error handling - -**Fallback strategy:** -1. Attempt resume with stored token -2. If resume fails → start new session -3. Emit warning message to user: "Session could not be fully resumed after model switch" - - ---- - -## Integration Points - -### WebSocket Integration - -| Layer | Change Required | -|-------|-----------------| -| Client (`apps/web/lib/ws/client.ts`) | No changes | -| Actions (`apps/web/lib/ws/actions.ts`) | Add `model` field to prompt action | -| Handler (`apps/backend/internal/ws/handlers/orchestrator.go`) | Parse `model` from payload | - -**Wire format example:** -```json -{ - "id": "uuid", - "type": "request", - "action": "orchestrator.prompt", - "payload": { - "task_id": "task-uuid", - "session_id": "session-uuid", - "content": "User message", - "model": "opus4.5" - } -} -``` - -### Store Integration - -**New store slice additions:** - -```typescript -// In store.ts -pendingModel: { - bySessionId: Record, // sessionId → pending model id - setPendingModel: (sessionId: string, modelId: string) => void, - clearPendingModel: (sessionId: string) => void, - getPendingModel: (sessionId: string) => string | null, -} -``` - -**Store → Component data flow:** -``` -ModelSelector reads: settingsAgents (for available models), session.agent_profile_id -ModelSelector writes: pendingModel.bySessionId[sessionId] -TaskChatPanel reads: pendingModel.bySessionId[sessionId] on prompt submit -``` - -### Database Integration - -**Decision: No schema change (runtime-only)** -- Model switch is runtime-only; session's `agent_profile_id` remains unchanged -- On backend restart, session resumes with original profile model -- No migration required - ---- - -## Challenges and Edge Cases - -### 1. Agent Busy During Model Switch Request - -**Scenario**: User selects new model while agent is processing a previous prompt. - -**Solution options:** -- **Queue the switch**: Store pending model, apply after current response completes -- **Block selection**: Disable model selector while agent is busy -- **Immediate abort**: Cancel current response, switch model (destructive) - -**Recommendation**: Queue the switch; apply on next user-initiated prompt. - -### 2. Rapid Model Changes - -**Scenario**: User changes model multiple times before sending a prompt. - -**Solution**: Only the final selection matters; each change overwrites `pendingModelBySessionId[sessionId]`. - -### 3. Resume Failure After Switch - -**Scenario**: Agent restarts with new model but ACP session/load fails. - -**Handling:** -1. Log error for debugging -2. Fall back to new session initialization -3. Show user message: "Model switched successfully but session context could not be fully restored" - -### 4. Different Agent Types - -**Scenario**: User tries to switch to a model not supported by current agent type. - -**Prevention**: Model selector only shows models from `agent.model_config.available_models` for the session's agent type. - -### 5. Session State Persistence - -**Scenario**: Backend restarts mid-session; what model should resume use? - -**Behavior**: Uses `agent_profile_snapshot.model` from session creation (original profile model). Runtime model switches are not persisted. - -### 6. Model Availability Changes - -**Scenario**: A model is removed from agent configuration while sessions are active. - -**Handling**: Validate model against current `available_models` before sending to backend. If invalid, fall back to session's original model. - ---- - -## Alternative Approaches - -### Alternative A: Runtime Model Switching via ACP - -**Concept**: Send a hypothetical `session/switch_model` ACP call without restarting agent. - -**Pros:** -- No process restart; faster switch -- Seamless context preservation - -**Cons:** -- Not supported by current agent implementations -- Would require agent CLI changes upstream -- Different agents would need different implementations - -**Verdict**: Not viable without significant upstream work. - -### Alternative B: Model as Separate Session Concept - -**Concept**: Treat each model switch as a "sub-session" within the same UI session. - -**Pros:** -- Clean separation of concerns -- Easy to track model history - -**Cons:** -- Complex UI implications -- Breaks current session model significantly -- Overhead for simple model changes - -**Verdict**: Over-engineering for the requirement. - -### Alternative C: Pre-select Model Before Session Start Only - -**Concept**: Allow model selection only at session creation, not mid-session. - -**Pros:** -- Simplest implementation -- No restart complexity - -**Cons:** -- Doesn't meet the stated requirement -- Poor UX for iterative work - -**Verdict**: Does not satisfy user requirements. - ---- - -## Implementation Phases - -### Phase 1: Frontend Model Selector (Est. 2-3 days) - -**Goal**: Model dropdown appears in UI; selection stored locally. - -**Files to modify:** -1. `apps/web/lib/state/store.ts` - Add pendingModel slice -2. `apps/web/components/task/task-chat-panel.tsx` - Add ModelSelector -3. Create `apps/web/components/task/model-selector.tsx` - New component - -**Deliverable**: User can select a model, but it has no effect yet. - -### Phase 2: Backend Prompt Extension (Est. 2-3 days) - -**Goal**: Prompts can include model; backend detects change. - -**Files to modify:** -1. `apps/web/lib/ws/actions.ts` - Add model to prompt payload -2. `apps/backend/internal/ws/handlers/orchestrator.go` - Parse model -3. `apps/backend/internal/orchestrator/service.go` - PromptTask accepts model - -**Deliverable**: Model parameter flows from UI to orchestrator. - -### Phase 3: Agent Restart Logic (Est. 3-4 days) - -**Goal**: Backend restarts agent with new model when detected. - -**Files to modify:** -1. `apps/backend/internal/orchestrator/executor/executor.go` - SwitchModel method -2. `apps/backend/internal/agent/lifecycle/manager.go` - Ensure clean restart -3. `apps/backend/internal/task/service/service.go` - Insert status message - -**Deliverable**: Model switch triggers agent restart; status message appears. - -### Phase 4: Session Resumption (Est. 2-3 days) - -**Goal**: Resume ACP session after agent restart (where supported). - -**Files to modify:** -1. `apps/backend/internal/agent/lifecycle/session.go` - Resume logic -2. `apps/backend/internal/orchestrator/executor/executor.go` - Handle resume failures - -**Deliverable**: Codex-style agents resume context; Auggie-style gracefully degrade. - -### Phase 5: Testing & Edge Cases (Est. 2-3 days) - -**Goal**: Verify all scenarios work correctly. - -**Test scenarios:** -- Model switch while agent idle -- Model switch while agent busy (queued) -- Rapid model changes -- Resume failure fallback -- Invalid model selection -- Backend restart mid-session - ---- - -## Summary of Files to Modify - -| File | Changes | -|------|---------| -| `apps/web/lib/state/store.ts` | Add pendingModel slice | -| `apps/web/components/task/task-chat-panel.tsx` | Integrate ModelSelector, pass model on prompt | -| `apps/web/components/task/model-selector.tsx` | New component | -| `apps/web/lib/ws/actions.ts` | Add model field to prompt action | -| `apps/backend/internal/ws/handlers/orchestrator.go` | Parse model from WS payload | -| `apps/backend/internal/orchestrator/service.go` | Extend PromptTask signature | -| `apps/backend/internal/orchestrator/executor/executor.go` | Add SwitchModel method | -| `apps/backend/internal/agent/lifecycle/manager.go` | Ensure clean restart support | -| `apps/backend/internal/agent/lifecycle/session.go` | Handle resume after restart | -| `apps/backend/internal/task/service/service.go` | Create model switch status message | - ---- - -## Estimated Total Effort - -| Phase | Estimate | -|-------|----------| -| Phase 1: Frontend Model Selector | 2-3 days | -| Phase 2: Backend Prompt Extension | 2-3 days | -| Phase 3: Agent Restart Logic | 3-4 days | -| Phase 4: Session Resumption | 2-3 days | -| Phase 5: Testing & Edge Cases | 2-3 days | -| **Total** | **11-16 days** | - ---- - -## Resolved Decisions - -| Decision | Resolution | -|----------|------------| -| Database schema | Runtime-only, no schema changes | -| Non-recoverable agents (Auggie) | Accept context loss, show "new session started" message | -| Agent busy during switch | Not an issue - model applied on next user prompt | -| UI placement | Replace existing agent selector with model selector | -| Confirmation dialog | None required - seamless switch | - diff --git a/docs/design/workflow-system.md b/docs/design/workflow-system.md deleted file mode 100644 index 57d2ccb3c6..0000000000 --- a/docs/design/workflow-system.md +++ /dev/null @@ -1,1772 +0,0 @@ -# Workflow System Design - -## Overview - -A **Workflow** defines the lifecycle of **task sessions** on a board through a sequence of **Steps**. Each step maps to a column and defines: -- **Behavior**: What happens when a session enters the step (auto-start agent, append prompts, etc.) -- **Transitions**: Rules for moving to next/previous steps - -### Session-Level Workflows - -**Important**: Workflows operate at the **session level**, not the task level. A task can have multiple sessions (different attempts, different agents, parallel work), and each session progresses through the workflow independently. - -``` -Task - ├── Session 1 (Claude) ──▶ Planning ──▶ Review ──▶ Implementation ──▶ Done ✓ - ├── Session 2 (Codex) ──▶ Planning ──▶ Review ──▶ (rejected, abandoned) - └── Session 3 (Claude) ──▶ Planning ──▶ (in progress...) ← PRIMARY -``` - -**Rationale**: -- A task might need multiple attempts before completion -- Different agents can work on the same task in parallel -- Review feedback and approval are per-session (each attempt is reviewed independently) -- Session history provides audit trail of all attempts - -### Primary Session - -Each task has a **primary session** - the session that represents the task's current workflow position and state. - -- **Default**: The latest created session automatically becomes primary -- **User can change**: Pin a different session as primary if needed -- **Determines task display**: Task card appears in the column of the primary session's step -- **Default for operations**: Approve/reject buttons operate on the primary session - -``` -Task: "Implement auth" -├── Session 1 (done) → archived, was primary before -├── Session 2 (abandoned) → user abandoned after rejection -└── Session 3 (active) → PRIMARY - task appears in this session's column -``` - -When a new session is created, it automatically becomes the primary session (latest attempt is most relevant). Other sessions become "alternative attempts" - viewable but not the main path. - -## Design Decisions - -### Columns ARE Workflow Steps - -Simplify the model: **columns and workflow steps are the same entity**. The `columns` table is replaced by `workflow_steps`. - -``` -Board → Workflow (required) - → Workflow Steps (these ARE the columns) -``` - -**Rationale**: -- **Simplicity**: One entity instead of two with a relationship -- **No indirection**: Step behavior is directly on the column -- **Cleaner data model**: Every board has a workflow, every column is a step - -### Rejection Behavior: Stay in Review - -When a user rejects at a review step, the **session stays in the same step** but gets marked with "changes requested" status. This enables iterative review cycles without moving the session back and forth. - -**How it works**: -1. User clicks "Request Changes" and provides feedback -2. Session stays in the review step -3. Session metadata is updated with rejection info -4. Agent can be re-triggered to address the feedback -5. User reviews again, can approve or request more changes -6. On approval, session moves to the configured `onApproval` step - -**Session metadata on rejection**: -```json -{ - "review_status": "changes_requested", - "review_feedback": [ - { - "id": "fb-1", - "message": "Need to handle edge case X", - "created_at": "2024-01-15T10:30:00Z", - "resolved": false - }, - { - "id": "fb-2", - "message": "Add unit tests for the new function", - "created_at": "2024-01-15T11:00:00Z", - "resolved": true - } - ], - "review_iteration": 2 -} -``` - -### Custom Workflows Supported - -Users can: -1. **Use a template as-is** - Quick start with pre-configured workflow -2. **Customize a template** - Clone a template and modify steps -3. **Create from scratch** - Blank workflow, add steps manually - -The `workflow_templates` table has `is_system` flag: -- `is_system = 1`: Built-in templates (read-only, can be cloned) -- `is_system = 0`: User-created templates (fully editable) - ---- - -## Key Concepts - -### 1. Workflow Template - -A pre-defined workflow type that boards can adopt. Examples: - -| Template | Description | Steps | -|----------|-------------|-------| -| `dev` | Software development | Backlog → Planning → Implementation → Review → Done | -| `architecture` | System design | Backlog → Research → Design → Review → Approved | -| `bug-fix` | Bug resolution | Triage → Reproduce → Fix → Verify → Done | -| `simple` | Basic kanban | Todo → In Progress → Done | - -### 2. Workflow Step - -Each step has: - -```typescript -interface WorkflowStep { - id: string; - name: string; // Display name (e.g., "Planning") - position: number; // Order in workflow - stepType: WorkflowStepType; // Semantic type (see below) - - // Behavior configuration - behavior: { - autoStartAgent: boolean; // Auto-start agent when task enters - promptPrefix?: string; // Prepended to user/task prompt - promptSuffix?: string; // Appended to user/task prompt - systemPromptOverride?: string; // Override agent's system prompt - planMode?: boolean; // Use plan mode (no execution) - requireApproval?: boolean; // Require user approval before proceeding - }; - - // Transition rules - transitions: { - onComplete?: string; // Step ID to move to on agent completion - onApproval?: string; // Step ID to move to on user approval - allowManualMove?: boolean; // Can user drag to any column? - }; - - // Visual - color: string; - taskState: TaskState; // Maps to existing TaskState enum -} -``` - -### 3. Step Types - -Semantic step types that define default behaviors: - -| Type | Default Behavior | -|------|------------------| -| `backlog` | No auto-action. Tasks wait here. | -| `planning` | Auto-start agent in plan mode. Produces `plan.md`. | -| `implementation` | Auto-start agent with implementation prompt. Uses plan if available. | -| `review` | Agent paused. Awaits user review of changes. | -| `verification` | Auto-start agent to verify/test changes. | -| `done` | Terminal state. No actions. | -| `blocked` | Task is blocked. No auto-actions. | - -## Workflow Templates - -### Dev Workflow (Default) - -```yaml -id: dev -name: Development -description: Standard software development workflow with planning phase -steps: - - id: backlog - name: Backlog - stepType: backlog - position: 0 - taskState: TODO - color: bg-neutral-400 - behavior: - autoStartAgent: false - transitions: - allowManualMove: true - - - id: planning - name: Planning - stepType: planning - position: 1 - taskState: IN_PROGRESS - color: bg-purple-500 - behavior: - autoStartAgent: true - planMode: true - promptPrefix: | - [PLANNING PHASE] - Analyze this task and create a detailed implementation plan. - Do NOT make any code changes yet - only analyze and plan. - - Create a plan that includes: - 1. Understanding of the requirements - 2. Files that need to be modified or created - 3. Step-by-step implementation approach - 4. Potential risks or considerations - - Save your plan to `.kandev/plans/{task_id}.md` - transitions: - onComplete: review-plan - allowManualMove: true - - - id: review-plan - name: Review Plan - stepType: review - position: 2 - taskState: REVIEW - color: bg-yellow-500 - behavior: - autoStartAgent: false - requireApproval: true - transitions: - onApproval: implementation - allowManualMove: true - - - id: implementation - name: Implementation - stepType: implementation - position: 3 - taskState: IN_PROGRESS - color: bg-blue-500 - behavior: - autoStartAgent: true - promptPrefix: | - [IMPLEMENTATION PHASE] - Implement the task according to the plan in `.kandev/plans/{task_id}.md`. - Follow the plan step by step. - transitions: - onComplete: review-code - allowManualMove: true - - - id: review-code - name: Code Review - stepType: review - position: 4 - taskState: REVIEW - color: bg-orange-500 - behavior: - autoStartAgent: false - requireApproval: true - transitions: - onApproval: done - allowManualMove: true - - - id: done - name: Done - stepType: done - position: 5 - taskState: COMPLETED - color: bg-green-500 - behavior: - autoStartAgent: false - transitions: - allowManualMove: true -``` - -### Architecture Workflow - -```yaml -id: architecture -name: Architecture -description: System design and architecture workflow -steps: - - id: backlog - name: Ideas - stepType: backlog - position: 0 - taskState: TODO - behavior: - autoStartAgent: false - - - id: research - name: Research - stepType: planning - position: 1 - taskState: IN_PROGRESS - behavior: - autoStartAgent: true - planMode: true - promptPrefix: | - [RESEARCH PHASE] - Research this architecture topic. Analyze: - 1. Current state of the codebase - 2. Industry best practices - 3. Trade-offs of different approaches - - Document your findings in `.kandev/research/{task_id}.md` - transitions: - onComplete: design - - - id: design - name: Design - stepType: planning - position: 2 - taskState: IN_PROGRESS - behavior: - autoStartAgent: true - planMode: true - promptPrefix: | - [DESIGN PHASE] - Based on research in `.kandev/research/{task_id}.md`, create a design: - 1. High-level architecture - 2. Component diagram - 3. API contracts - 4. Migration path (if applicable) - - Save design to `.kandev/designs/{task_id}.md` - transitions: - onComplete: review - - - id: review - name: Review - stepType: review - position: 3 - taskState: REVIEW - behavior: - requireApproval: true - transitions: - onApproval: approved - - - id: approved - name: Approved - stepType: done - position: 4 - taskState: COMPLETED -``` - -## Data Model - -### Schema - -```sql --- Workflow templates (system + user-defined) --- These are blueprints that can be used when creating a board -CREATE TABLE workflow_templates ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - description TEXT, - is_system INTEGER DEFAULT 0, -- System templates are read-only - steps JSON NOT NULL, -- Array of step definitions (blueprint) - created_at TIMESTAMP, - updated_at TIMESTAMP -); - --- Boards now require a workflow --- The workflow_template_id tracks which template was used (for reference only) -CREATE TABLE boards ( - id TEXT PRIMARY KEY, - workspace_id TEXT NOT NULL REFERENCES workspaces(id), - name TEXT NOT NULL, - description TEXT, - workflow_template_id TEXT REFERENCES workflow_templates(id), -- Template used (nullable for custom) - created_at TIMESTAMP, - updated_at TIMESTAMP -); - --- Workflow steps replace columns entirely --- Each step IS a column with behavior attached -CREATE TABLE workflow_steps ( - id TEXT PRIMARY KEY, - board_id TEXT NOT NULL REFERENCES boards(id) ON DELETE CASCADE, - name TEXT NOT NULL, - step_type TEXT NOT NULL, -- backlog, planning, implementation, review, done, blocked - position INTEGER NOT NULL, - color TEXT NOT NULL, - - -- Behavior configuration - auto_start_agent INTEGER DEFAULT 0, - plan_mode INTEGER DEFAULT 0, - require_approval INTEGER DEFAULT 0, - prompt_prefix TEXT, - prompt_suffix TEXT, - - -- Transition rules - on_complete_step_id TEXT REFERENCES workflow_steps(id), -- Step to move to on agent completion - on_approval_step_id TEXT REFERENCES workflow_steps(id), -- Step to move to on approval - allow_manual_move INTEGER DEFAULT 1, - - created_at TIMESTAMP, - updated_at TIMESTAMP -); - --- Tasks remain simple - workflow progress is tracked per-session -CREATE TABLE tasks ( - id TEXT PRIMARY KEY, - workspace_id TEXT NOT NULL REFERENCES workspaces(id), - board_id TEXT NOT NULL REFERENCES boards(id), - title TEXT NOT NULL, - description TEXT, - priority INTEGER DEFAULT 0, - position INTEGER DEFAULT 0, - metadata JSON, - created_at TIMESTAMP, - updated_at TIMESTAMP -); - --- Task sessions track workflow progress (each session goes through workflow independently) --- Extends existing task_sessions table with workflow fields -CREATE TABLE task_sessions ( - id TEXT PRIMARY KEY, - task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, - - -- Primary session flag (NEW) - is_primary BOOLEAN DEFAULT FALSE, -- Only one session per task should be primary - - -- Workflow tracking (NEW) - workflow_step_id TEXT REFERENCES workflow_steps(id), -- Current step in workflow - review_status TEXT, -- NULL, 'pending', 'changes_requested', 'approved' - review_feedback JSON, -- Array of feedback items - - -- Existing fields - agent_profile_id TEXT NOT NULL, - executor_id TEXT, - environment_id TEXT, - repository_id TEXT, - base_branch TEXT, - agent_profile_snapshot JSON, - executor_snapshot JSON, - environment_snapshot JSON, - repository_snapshot JSON, - state TEXT NOT NULL DEFAULT 'CREATED', -- CREATED, RUNNING, COMPLETED, etc. - error_message TEXT, - metadata JSON, - started_at TIMESTAMP NOT NULL, - completed_at TIMESTAMP, - updated_at TIMESTAMP NOT NULL -); - --- Index to quickly find primary session for a task -CREATE INDEX idx_task_sessions_primary ON task_sessions(task_id, is_primary) WHERE is_primary = TRUE; - --- Session step history (audit trail per session) -CREATE TABLE session_step_history ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id TEXT NOT NULL REFERENCES task_sessions(id) ON DELETE CASCADE, - from_step_id TEXT REFERENCES workflow_steps(id), - to_step_id TEXT NOT NULL REFERENCES workflow_steps(id), - trigger TEXT NOT NULL, -- manual, auto_complete, approval - actor_id TEXT, -- User or system - metadata JSON, - created_at TIMESTAMP -); -``` - -### Key Changes from Current Schema - -| Current | New | -|---------|-----| -| `columns` table | Replaced by `workflow_steps` | -| `task.column_id` | Removed (tasks don't track workflow position) | -| No workflow on sessions | `task_sessions.workflow_step_id` tracks session's position | -| No review status | `task_sessions.review_status` + `review_feedback` | -| Column has `state` field | Step has `step_type` + behavior fields | -| No workflow concept | `workflow_templates` + board has `workflow_template_id` | -| No primary session | `task_sessions.is_primary` flag (one per task) | - -### Primary Session and Task Display - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ Task: "Implement user authentication" │ -│ Board: Development Board │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Session 1 (completed) │ -│ ├── is_primary: FALSE │ -│ ├── workflow_step: "Done" ✓ │ -│ └── review_status: "approved" │ -│ │ -│ Session 2 (abandoned) │ -│ ├── is_primary: FALSE │ -│ ├── workflow_step: "Review Plan" │ -│ └── review_status: "changes_requested" │ -│ │ -│ Session 3 (active) ← PRIMARY │ -│ ├── is_primary: TRUE │ -│ ├── workflow_step: "Implementation" │ -│ └── review_status: NULL │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ - -Task's kanban position = PRIMARY session's workflow_step -(latest session is automatically primary; user can change) -``` - -## Backend Components - -### 1. Workflow Service (`internal/workflow/`) - -```go -package workflow - -type Service struct { - repo Repository - sessionService *task.SessionService - orchestrator *orchestrator.Service - eventBus *events.Bus -} - -// Core operations -func (s *Service) CreateWorkflowFromTemplate(ctx, boardID, templateID string) error -func (s *Service) GetBoardWorkflow(ctx, boardID string) (*Workflow, error) -func (s *Service) GetStepForColumn(ctx, columnID string) (*WorkflowStep, error) - -// Primary session operations -func (s *Service) GetPrimarySession(ctx, taskID string) (*TaskSession, error) -func (s *Service) SetPrimarySession(ctx, taskID, sessionID string) error // Unsets previous primary - -// Session transitions (workflow operates at session level) -func (s *Service) StartSession(ctx, taskID string, stepID string) (*TaskSession, error) // Auto-sets as primary -func (s *Service) MoveSessionToStep(ctx, sessionID, toStepID string) error -func (s *Service) OnAgentComplete(ctx, sessionID string) error -func (s *Service) ApproveSession(ctx, sessionID string) error -func (s *Service) RejectSession(ctx, sessionID, reason string) error -func (s *Service) GetSessionStep(ctx, sessionID string) (*WorkflowStep, error) -``` - -### 2. Step Executor (`internal/workflow/executor/`) - -Handles step-specific behaviors for sessions: - -```go -type StepExecutor struct { - orchestrator *orchestrator.Service -} - -func (e *StepExecutor) ExecuteStep(ctx, session *TaskSession, task *Task, step *WorkflowStep) error { - // 1. Apply prompt modifications - prompt := e.buildPrompt(task, session, step) - - // 2. Start agent if autoStartAgent - if step.Behavior.AutoStartAgent { - return e.orchestrator.StartSession(ctx, session.ID, prompt, step.Behavior.PlanMode) - } - - return nil -} - -func (e *StepExecutor) buildPrompt(task *Task, session *TaskSession, step *WorkflowStep) string { - var sb strings.Builder - - if step.Behavior.PromptPrefix != "" { - sb.WriteString(e.interpolate(step.Behavior.PromptPrefix, task, session)) - sb.WriteString("\n\n") - } - - sb.WriteString(task.Description) - - // Include previous feedback if session has changes_requested - if session.ReviewStatus == "changes_requested" && len(session.ReviewFeedback) > 0 { - sb.WriteString("\n\n[FEEDBACK TO ADDRESS]\n") - for _, fb := range session.ReviewFeedback { - if !fb.Resolved { - sb.WriteString("- " + fb.Message + "\n") - } - } - } - - if step.Behavior.PromptSuffix != "" { - sb.WriteString("\n\n") - sb.WriteString(e.interpolate(step.Behavior.PromptSuffix, task, session)) - } - - return sb.String() -} -``` - -### 3. Transition Handler - -Listens for events and triggers session transitions: - -```go -func (s *Service) handleAgentComplete(event *events.AgentCompleteEvent) { - // Workflow operates at session level, not task level - step := s.GetSessionStep(ctx, event.SessionID) - - if step.Transitions.OnComplete != "" { - nextStep := s.GetStep(ctx, step.Transitions.OnComplete) - s.MoveSessionToStep(ctx, event.SessionID, nextStep.ID) - } -} -``` - -## Frontend Design Diagrams - -### User Flow: Board Creation with Workflow - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ BOARD CREATION FLOW │ -└─────────────────────────────────────────────────────────────────────────────┘ - - ┌──────────────┐ ┌───────────────────┐ ┌──────────────────────────┐ - │ Click "New │────▶│ Board Creation │────▶│ Select Workflow │ - │ Board" │ │ Dialog Opens │ │ Template │ - └──────────────┘ └───────────────────┘ └──────────────────────────┘ - │ - ┌────────────────────────────────────┼────────────────┐ - │ │ │ - ▼ ▼ ▼ - ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ - │ "Development" │ │ "Architecture" │ │ "Custom" │ - │ │ │ │ │ │ - │ Backlog │ │ Ideas │ │ (Empty board, │ - │ Planning │ │ Research │ │ add steps │ - │ Review Plan │ │ Design │ │ manually) │ - │ Implementation │ │ Review │ │ │ - │ Code Review │ │ Approved │ │ │ - │ Done │ │ │ │ │ - └─────────────────┘ └─────────────────┘ └─────────────────┘ - │ │ │ - └────────────────────────────────────┼────────────────┘ - │ - ▼ - ┌──────────────────────────┐ - │ Preview Workflow Steps │ - │ (icons show behaviors) │ - │ │ - │ 🤖 = auto-start agent │ - │ ✓ = requires approval │ - │ 📋 = plan mode │ - └──────────────────────────┘ - │ - ▼ - ┌──────────────────────────┐ - │ Click "Create Board" │ - │ │ - │ → Creates board │ - │ → Creates workflow_steps│ - │ → Redirects to board │ - └──────────────────────────┘ -``` - -### User Flow: Session Lifecycle Through Workflow - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ SESSION LIFECYCLE (Development Workflow) │ -│ │ -│ Note: Each SESSION progresses through steps independently. │ -│ A task can have multiple sessions at different steps. │ -└─────────────────────────────────────────────────────────────────────────────┘ - - ┌─────────┐ ┌──────────┐ ┌─────────────┐ ┌──────────────┐ ┌──────────┐ ┌──────┐ - │ BACKLOG │───▶│ PLANNING │───▶│ REVIEW PLAN │───▶│IMPLEMENTATION│───▶│CODE REVIEW│───▶│ DONE │ - └─────────┘ └──────────┘ └─────────────┘ └──────────────┘ └──────────┘ └──────┘ - │ │ │ │ │ │ - │ │ │ │ │ │ - ▼ ▼ ▼ ▼ ▼ ▼ - ┌─────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌─────────┐ - │ User │ │ 🤖 Agent │ │ ✓ User │ │ 🤖 Agent │ │ ✓ User │ │ Session │ - │ starts │ │ auto- │ │ reviews │ │ auto- │ │ reviews │ │ complete│ - │ new │ │ starts │ │ plan │ │ starts │ │ code │ │ │ - │ session │ │ (plan │ │ │ │ (uses │ │ │ │ │ - │ │ │ mode) │ │ │ │ plan) │ │ │ │ │ - └─────────┘ └───────────┘ └───────────┘ └───────────┘ └───────────┘ └─────────┘ - │ │ │ │ - ▼ │ ▼ │ - ┌───────────┐ │ ┌───────────┐ │ - │ Creates │ │ │ Makes │ │ - │ plan.md │ │ │ code │ │ - │ in work- │ │ │ changes │ │ - │ tree │ │ │ │ │ - └───────────┘ │ └───────────┘ │ - │ │ │ - ▼ ▼ ▼ - ┌───────────┐ ┌─────────────┐ ┌─────────────┐ - │ On agent │ │ Approve │ │ Approve │ - │ complete │ │ session? │ │ session? │ - │ → session │ │ │ │ │ - │ moves to │ │ YES → next │ │ YES → Done │ - │ Review │ │ NO → stay │ │ NO → stay │ - │ Plan │ │ + feedback │ + feedback - └───────────┘ └─────────────┘ └─────────────┘ - - ┌─────────────────────────────────────────────────────────────────────────────┐ - │ MULTIPLE SESSIONS EXAMPLE: │ - │ │ - │ Task: "Implement auth" │ - │ ├── Session 1: ────▶ Planning ────▶ Review ────▶ Implementation ── (done) │ - │ ├── Session 2: ────▶ Planning ────▶ Review (rejected, abandoned) │ - │ └── Session 3: ────▶ Planning (in progress...) ← PRIMARY │ - │ │ - │ Task card shows in column of PRIMARY session (Session 3 → "Planning") │ - │ User can change primary session if needed (e.g., pin Session 1 as primary) │ - └─────────────────────────────────────────────────────────────────────────────┘ -``` - -### User Flow: Session Review and Rejection Cycle - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ SESSION REVIEW & REJECTION FLOW │ -│ │ -│ Review and approval happen per-SESSION, not per-task. │ -│ Each session's plan/work is reviewed independently. │ -└─────────────────────────────────────────────────────────────────────────────┘ - - ┌─────────────────────┐ - │ Session in REVIEW │ - │ PLAN step │ - └─────────────────────┘ - │ - ▼ - ┌─────────────────────┐ - │ User opens task │ - │ → selects session │ - │ to review │ - └─────────────────────┘ - │ - ▼ - ┌─────────────────────────────────────────┐ - │ SESSION REVIEW PANEL │ - │ ┌─────────────────────────────────┐ │ - │ │ Session: #3 (Claude 3.5) │ │ - │ │ Step: Review Plan │ │ - │ │ ───────────────────────────── │ │ - │ │ 📋 Implementation Plan │ │ - │ │ ## Overview │ │ - │ │ This task will implement... │ │ - │ │ ## Steps │ │ - │ │ 1. Create new component │ │ - │ │ 2. Add API endpoint │ │ - │ │ ... │ │ - │ └─────────────────────────────────┘ │ - │ │ - │ ┌──────────────┐ ┌──────────────────┐ │ - │ │ ✓ Approve │ │ ✗ Request │ │ - │ │ Session │ │ Changes │ │ - │ └──────────────┘ └──────────────────┘ │ - └─────────────────────────────────────────┘ - │ - ┌────────────────┴────────────────┐ - │ │ - ▼ ▼ - ┌─────────────────┐ ┌─────────────────────┐ - │ APPROVE │ │ REQUEST CHANGES │ - │ │ │ │ - │ Session moves │ │ Feedback dialog │ - │ to next step │ │ opens │ - │ (IMPLEMENTATION)│ │ │ - └─────────────────┘ └─────────────────────┘ - │ - ▼ - ┌─────────────────────┐ - │ Enter feedback: │ - │ ┌─────────────────┐│ - │ │ "Need to handle ││ - │ │ edge case X" ││ - │ └─────────────────┘│ - │ [Submit Feedback] │ - └─────────────────────┘ - │ - ▼ - ┌─────────────────────────────────────────┐ - │ Session stays in REVIEW PLAN step │ - │ with "Changes Requested" status │ - │ │ - │ ┌─────────────────────────────────┐ │ - │ │ Session #3: Claude 3.5 │ │ - │ │ ┌─────────────────────────────┐ │ │ - │ │ │ 🔴 Changes Requested (1) │ │ │ - │ │ └─────────────────────────────┘ │ │ - │ └─────────────────────────────────┘ │ - └─────────────────────────────────────────┘ - │ - ▼ - ┌─────────────────────────────────────────┐ - │ SESSION REVIEW PANEL (updated) │ - │ │ - │ ⚠️ Session Feedback History │ - │ ┌─────────────────────────────────┐ │ - │ │ 🟡 "Need to handle edge case X" │ │ - │ │ 2 minutes ago │ │ - │ │ [Mark Resolved] │ │ - │ └─────────────────────────────────┘ │ - │ │ - │ ┌─────────────────────────────────┐ │ - │ │ 🤖 Re-run Session Agent │ │ - │ │ to Address Feedback │ │ - │ └─────────────────────────────────┘ │ - │ │ - │ ⚠️ 1 unresolved feedback item. │ - │ Resolve all before approving. │ - └─────────────────────────────────────────┘ - │ - ▼ - ┌─────────────────────────────────────────┐ - │ User clicks "Re-run Session Agent" │ - │ → Agent restarts in same session │ - │ → Agent sees previous plan + feedback │ - │ → Agent updates plan file │ - │ → On complete, user reviews again │ - └─────────────────────────────────────────┘ -``` - -### UI Component Layout: Kanban Board - -``` -┌─────────────────────────────────────────────────────────────────────────────────────────────┐ -│ 📋 My Project Board [⚙️ Settings] │ -├─────────────────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ ○ Backlog │ │ ○ Planning │ │ ○ Review │ │ ○ Implement │ │ ○ Done │ │ -│ │ │ │ 🤖 📋 │ │ ✓ │ │ 🤖 │ │ │ │ -│ ├─────────────┤ ├─────────────┤ ├─────────────┤ ├─────────────┤ ├─────────────┤ │ -│ │ │ │ │ │ │ │ │ │ │ │ -│ │ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │ │ -│ │ │ Task A │ │ │ │ Task B │ │ │ │ Task D │ │ │ │ Task E │ │ │ │ Task G │ │ │ -│ │ │ (no │ │ │ │ ●●○○○○ │ │ │ │ 🔴 (1) │ │ │ │ ●●●●○○ │ │ │ │ ●●●●●● │ │ │ -│ │ │ session)│ │ │ │ 🤖 S#1 │ │ │ │ S#2 │ │ │ │ 🤖 S#3 │ │ │ │ ✓ S#1 │ │ │ -│ │ └─────────┘ │ │ └─────────┘ │ │ └─────────┘ │ │ └─────────┘ │ │ └─────────┘ │ │ -│ │ │ │ │ │ │ │ │ │ │ │ -│ │ ┌─────────┐ │ │ ┌─────────┐ │ │ │ │ │ │ ┌─────────┐ │ │ -│ │ │ Task C │ │ │ │ Task F │ │ │ │ │ │ │ │ Task H │ │ │ -│ │ │ (no │ │ │ │ ●●○○○○ │ │ │ │ │ │ │ │ ●●●●●● │ │ │ -│ │ │ session)│ │ │ │ S#1 │ │ │ │ │ │ │ │ ✓ S#2 │ │ │ -│ │ └─────────┘ │ │ └─────────┘ │ │ │ │ │ │ └─────────┘ │ │ -│ │ │ │ │ │ │ │ │ │ │ │ -│ │ [+ Add] │ │ │ │ │ │ │ │ │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────────────────────────┘ - -LEGEND: - ○ Column color indicator 🤖 Agent auto-starts in this column - 📋 Plan mode enabled ✓ Requires approval - ●●●○○○ Session workflow progress 🔴 (1) Changes requested (on session) - 🤖 S#1 Active session running S#2 Session indicator (shows primary session) - ★ Primary session indicator - -NOTE: Task card position in column = PRIMARY session's workflow step - Latest session is automatically primary; user can change - Click task → select specific session to view/manage -``` - -### UI Component Layout: Task Detail Panel (Session-Based) - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ ← Back [⋮ More] │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Implement user authentication │ -│ ════════════════════════════════════════════════════════════════════════ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────────┐ │ -│ │ Sessions [+ New Session]│ │ -│ ├─────────────────────────────────────────────────────────────────────┤ │ -│ │ │ │ -│ │ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │ │ -│ │ │ ● Session #3 │ │ ○ Session #2 │ │ ○ Session #1 │ │ │ -│ │ │ Claude 3.5 │ │ Codex │ │ Claude 3.5 │ │ │ -│ │ │ In: Review │ │ Abandoned │ │ ✓ Completed │ │ │ -│ │ │ 🔴 (1 issue) │ │ │ │ │ │ │ -│ │ └────────────────┘ └────────────────┘ └────────────────┘ │ │ -│ │ │ │ -│ └─────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ═══════════════════════════════════════════════════════════════════════ │ -│ SESSION #3 DETAILS │ -│ ═══════════════════════════════════════════════════════════════════════ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────────┐ │ -│ │ Session Workflow Progress │ │ -│ │ │ │ -│ │ ✓ Backlog → ✓ Planning → ● Review → ○ Implement → ○ Done │ │ -│ │ Plan │ │ -│ └─────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────────┐ │ -│ │ ⚠️ Session Feedback History │ │ -│ ├─────────────────────────────────────────────────────────────────────┤ │ -│ │ │ │ -│ │ ┌───────────────────────────────────────────────────────────────┐ │ │ -│ │ │ 🟡 "Consider using httpOnly cookies for refresh tokens" │ │ │ -│ │ │ 5 minutes ago │ │ │ -│ │ │ [Mark Resolved] │ │ │ -│ │ └───────────────────────────────────────────────────────────────┘ │ │ -│ │ │ │ -│ │ ┌───────────────────────────────────────────────────────────────┐ │ │ -│ │ │ ✅ "Add rate limiting to login endpoint" │ │ │ -│ │ │ 1 hour ago Resolved │ │ │ -│ │ └───────────────────────────────────────────────────────────────┘ │ │ -│ │ │ │ -│ └─────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────────┐ │ -│ │ │ │ -│ │ ⚠️ 1 unresolved feedback item. Resolve all before approving. │ │ -│ │ │ │ -│ │ ┌─────────────────────────┐ ┌─────────────────────────────────┐ │ │ -│ │ │ 🤖 Re-run Session │ │ [Abandon Session] │ │ │ -│ │ │ Agent │ │ │ │ │ -│ │ └─────────────────────────┘ └─────────────────────────────────┘ │ │ -│ │ │ │ -│ │ ┌─────────────────────────┐ ┌─────────────────────────────────┐ │ │ -│ │ │ ✓ Approve Session │ │ ✗ Request Changes │ │ │ -│ │ │ (disabled) │ │ │ │ │ -│ │ └─────────────────────────┘ └─────────────────────────────────┘ │ │ -│ │ │ │ -│ └─────────────────────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -### UI Component Layout: Workflow Editor (Board Settings) - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ ⚙️ Board Settings │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ General │ Workflow │ Permissions │ -│ ──────── ════════ ─────────── │ -│ │ -│ Workflow Steps [+ Add Step] │ -│ ═══════════════════════════════════════════════════════════════════════ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────────┐ │ -│ │ ⋮⋮ │ 🟢 │ Backlog │ backlog ▼ │ │ 🗑️ │ ▼ │ │ -│ └─────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────────┐ │ -│ │ ⋮⋮ │ 🔵 │ Planning │ planning ▼ │ 🤖 📋 │ 🗑️ │ ▼ │ │ -│ ├─────────────────────────────────────────────────────────────────────┤ │ -│ │ │ │ -│ │ ☑️ Auto-start agent ☑️ Plan mode (no execution) │ │ -│ │ ☐ Require approval │ │ -│ │ │ │ -│ │ Prompt Prefix: │ │ -│ │ ┌───────────────────────────────────────────────────────────────┐ │ │ -│ │ │ [PLANNING PHASE] Analyze the task and create a detailed │ │ │ -│ │ │ implementation plan. Do NOT write any code yet. │ │ │ -│ │ └───────────────────────────────────────────────────────────────┘ │ │ -│ │ │ │ -│ │ On Completion, move to: [ Review Plan ▼ ] │ │ -│ │ │ │ -│ └─────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────────┐ │ -│ │ ⋮⋮ │ 🟡 │ Review Plan │ review ▼ │ ✓ │ 🗑️ │ ▶ │ │ -│ └─────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────────┐ │ -│ │ ⋮⋮ │ 🟣 │ Implementation │ implement ▼ │ 🤖 │ 🗑️ │ ▶ │ │ -│ └─────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────────┐ │ -│ │ ⋮⋮ │ 🟠 │ Code Review │ review ▼ │ ✓ │ 🗑️ │ ▶ │ │ -│ └─────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────────┐ │ -│ │ ⋮⋮ │ ⬜ │ Done │ done ▼ │ │ 🗑️ │ ▶ │ │ -│ └─────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ │ -│ LEGEND: │ -│ ⋮⋮ = Drag handle 🤖 = Auto-start agent 📋 = Plan mode │ -│ ✓ = Requires approval ▼/▶ = Expand/collapse step details │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -### State Diagram: Session in Review Step - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ SESSION STATE IN REVIEW STEP │ -└─────────────────────────────────────────────────────────────────────────────┘ - - ┌──────────────────────┐ - │ │ - │ PENDING_REVIEW │◀─────────────────────────┐ - │ │ │ - └──────────────────────┘ │ - │ │ - ┌───────────────┴───────────────┐ │ - │ │ │ - ▼ ▼ │ - ┌──────────────────┐ ┌──────────────────┐ │ - │ │ │ │ │ - │ APPROVED │ │ CHANGES_REQUESTED│ │ - │ │ │ │ │ - └──────────────────┘ └──────────────────┘ │ - │ │ │ - │ │ │ - ▼ ▼ │ - ┌──────────────────┐ ┌──────────────────┐ │ - │ │ │ │ │ - │ Move session to │ │ Agent re-runs │──────────────┘ - │ next step │ │ with feedback │ - │ │ │ │ - └──────────────────┘ └──────────────────┘ - - - Session Fields (in task_sessions table): - ┌─────────────────────────────────────────────────────────────────────┐ - │ workflow_step_id: "step-review-plan" -- Current workflow position │ - │ review_status: "changes_requested" -- pending | approved | ... │ - │ review_feedback: [ │ - │ { │ - │ "id": "fb-1", │ - │ "message": "Need to handle edge case X", │ - │ "created_at": "2024-01-15T10:30:00Z", │ - │ "resolved": false │ - │ }, │ - │ { │ - │ "id": "fb-2", │ - │ "message": "Add error handling", │ - │ "created_at": "2024-01-15T09:00:00Z", │ - │ "resolved": true, │ - │ "resolved_at": "2024-01-15T10:00:00Z" │ - │ } │ - │ ] │ - └─────────────────────────────────────────────────────────────────────┘ - - NOTE: Each session tracks its own workflow position independently. - A task's position on the kanban board is derived from its most - advanced active session. -``` - -### Data Flow: Session Start with Agent Auto-Start - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ DATA FLOW: SESSION START WITH AGENT AUTO-START │ -└─────────────────────────────────────────────────────────────────────────────┘ - - ┌──────────────┐ - │ Frontend │ - │ (Task Panel) │ - └──────────────┘ - │ - │ 1. User clicks "New Session" or "Start" on task - │ (selects target step, e.g., "Planning") - │ - ▼ - ┌──────────────┐ 2. WS: session.create ┌──────────────┐ - │ WebSocket │ ──────────────────────────────────────▶ │ Backend │ - │ Client │ { task_id, step_id } │ (WS GW) │ - └──────────────┘ └──────────────┘ - │ - │ 3. WorkflowService.StartSession() - ▼ - ┌──────────────┐ - │ Workflow │ - │ Service │ - │ │ - │ Creates new │ - │ TaskSession │ - │ with step_id │ - └──────────────┘ - │ - │ 4. Get target step config - ▼ - ┌──────────────┐ - │ WorkflowStep │ - │ auto_start: │ - │ true │ - │ plan_mode: │ - │ true │ - │ prompt_ │ - │ prefix:... │ - └──────────────┘ - │ - │ 5. Publish event - ▼ - ┌──────────────┐ - │ Event Bus │ - │ │ - │ session. │ - │ created │ - └──────────────┘ - │ - │ 6. Orchestrator handles event - ▼ - ┌──────────────┐ - │ Orchestrator │ - │ │ - │ if step. │ - │ auto_start: │ - │ LaunchAgent│ - └──────────────┘ - │ - │ 7. Build prompt with prefix + feedback - ▼ - ┌──────────────────────────────┐ - │ Prompt = step.prompt_prefix │ - │ + task.description │ - │ + session.feedback │ - │ + step.prompt_suffix │ - │ │ - │ "[PLANNING PHASE] Analyze │ - │ the task and create a │ - │ detailed implementation │ - │ plan. Do NOT write code.\n" │ - │ + "Implement user auth..." │ - │ + "[FEEDBACK TO ADDRESS]..."│ - └──────────────────────────────┘ - │ - │ 8. Start agent for session - ▼ - ┌──────────────┐ - │ Lifecycle │ - │ Manager │ - │ │ - │ StartSession │ - │ (session has │ - │ workflow_ │ - │ step_id) │ - └──────────────┘ - │ - ┌────────────────────────────────────────────────────────┘ - │ 9. WS: session.started, session.update (streaming) - ▼ - ┌──────────────┐ - │ Frontend │ - │ (Updates │ - │ session in │ - │ task panel │ - │ with 🤖) │ - └──────────────┘ - - NOTE: Task card position on kanban = PRIMARY session's workflow step -``` - -## Frontend Integration - -### Board Creation with Workflow Selection - -When creating a new board, users select a workflow template that determines the columns and their behaviors. - -**Dialog Flow**: -1. User clicks "New Board" -2. Dialog opens with board name input and workflow template selector -3. Selecting a template shows a preview of the workflow steps -4. User can choose "Custom" to start with a blank workflow -5. On create, columns are generated from the workflow steps - -```tsx - - - - Create Board - - -
- - -
- - - -
- Standard - - Todo → Plan → Implementation → Done - -
-
- -
- Architecture - - Ideas → Research → Design → Review → Approved - -
-
- -
- Simple Kanban - - Todo → In Progress → Done (no automation) - -
-
- -
- Custom - - Start with a blank workflow and add steps manually - -
-
-
-
- - {/* Workflow Preview */} - {selectedTemplate && selectedTemplate !== 'custom' && ( -
- -
- {templateSteps.map((step, i) => ( - -
- {step.name} - {step.behavior.autoStartAgent && ( - - )} - {step.behavior.requireApproval && ( - - )} -
- {i < templateSteps.length - 1 && ( - - )} -
- ))} -
-
- Auto-starts agent - Requires approval -
-
- )} -
- - - - - -
-
-``` - -### Kanban Board with Workflow Columns - -The kanban board displays columns based on workflow steps. Each column header shows: -- Column name -- Step type indicator (icon) -- Auto-start badge if enabled - -```tsx -
- {columns.map((column) => ( -
- {/* Column Header */} -
-
-
- {column.name} - - {column.step?.stepType} - -
-
- {column.step?.behavior.autoStartAgent && ( - - - - )} - {column.step?.behavior.requireApproval && ( - - - - )} -
-
- - {/* Task Cards */} -
- {column.tasks.map((task) => ( - - ))} -
-
- ))} -
-``` - -### Task Card with Primary Session Workflow Status - -Task cards show workflow-specific information based on the **primary session**: -- Primary session workflow progress dots -- Review status badge (if primary session has changes requested) -- Primary session indicator (★ marker) -- Agent status (running, waiting, etc.) - -```tsx -function TaskCard({ task, step }: { task: Task; step: WorkflowStep }) { - // Get the primary session for display (latest session by default) - const primarySession = usePrimarySession(task.id); - const sessionStep = primarySession?.workflow_step_id; - const hasChangesRequested = primarySession?.review_status === 'changes_requested'; - const feedbackCount = primarySession?.review_feedback?.filter(f => !f.resolved).length ?? 0; - const sessionCount = task.sessions?.length ?? 0; - - return ( - - {/* Header */} -
-

{task.title}

- {hasChangesRequested && ( - - Changes Requested ({feedbackCount}) - - )} -
- - {/* Primary Session Workflow Progress Dots */} - {primarySession && ( -
- {workflowSteps.map((s, i) => ( -
currentStepIndex && "bg-gray-300" // Future - )} - /> - ))} -
- )} - - {/* Primary Session Indicator */} -
- {primarySession ? ( - <> - ★ Session #{primarySession.number} - {sessionCount > 1 && ( - - +{sessionCount - 1} more - - )} - - ) : ( - No sessions - )} -
- - {/* Primary Session Agent Status */} - {primarySession && ( -
- {primarySession.state === 'RUNNING' && ( - <> - - Agent working... - - )} - {primarySession.state === 'WAITING_FOR_INPUT' && ( - <> - - Waiting for input - - )} -
- )} - - ); -} -``` - -### Review Step UI - Session Review Panel - -When a **session** is in a review step, the session detail panel shows: -1. Previous feedback (if any) -2. Approval actions - -```tsx -// Session-based review panel - operates on a specific session, not the task -function SessionReviewPanel({ session, task, step }: { - session: TaskSession; - task: Task; - step: WorkflowStep -}) { - return ( -
- {/* Session Info Header */} -
- - Session #{session.number} • {session.agent_profile?.name ?? 'Agent'} - - - {session.review_status ?? 'pending'} - -
- - {/* Session Feedback History */} - {existingFeedback.length > 0 && ( -
- - {existingFeedback.map((fb) => ( -
-
-

{fb.message}

- {fb.resolved ? ( - Resolved - ) : ( - - )} -
- - {formatRelativeTime(fb.created_at)} - -
- ))} -
- )} - - {/* Session Approval Actions */} -
- {!showFeedbackInput ? ( -
- - -
- ) : ( -
-