Restructure src/ modules with consistent internal organization
Reorganize major src/ directories to follow a consistent pattern with
core/, menu/, submenu/, inputs/, logs/, layout/, feedback/ subdirectories.
Changes by module:
- stores/: Move 5 store files to stores/core/
- utils/: Create core/ (terminal, tools, etc.) and menu/ (progress-bar)
- api/: Create copilot/core/, copilot/auth/, ollama/core/
- providers/: Create core/, copilot/core/, copilot/auth/, ollama/core/, login/core/
- ui/: Create core/, banner/core/, banner/menu/, spinner/core/,
input-editor/core/, components/core/, components/menu/
- tools/: Create core/ for registry.ts and types.ts
- tui-solid/: Reorganize components/ into menu/, submenu/, inputs/,
logs/, modals/, panels/, layout/, feedback/
- commands/: Create core/ for runner.ts and handlers.ts
- services/: Create core/ for agent.ts, permissions.ts, session.ts,
executor.ts, config.ts
All imports updated to use new paths. TypeScript compilation verified.
This commit is contained in:
6
src/api/copilot/auth/index.ts
Normal file
6
src/api/copilot/auth/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copilot API Auth - Token and Authentication
|
||||
*/
|
||||
|
||||
export * from "./token";
|
||||
export * from "./auth";
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
ChatCompletionResponse,
|
||||
StreamChunk,
|
||||
} from "@/types/providers";
|
||||
import { buildCopilotHeaders } from "@api/copilot/token";
|
||||
import { buildCopilotHeaders } from "@api/copilot/auth/token";
|
||||
|
||||
interface FormattedMessage {
|
||||
role: string;
|
||||
6
src/api/copilot/core/index.ts
Normal file
6
src/api/copilot/core/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copilot API Core - Chat and Models API
|
||||
*/
|
||||
|
||||
export * from "./chat";
|
||||
export * from "./models";
|
||||
@@ -5,18 +5,18 @@
|
||||
export {
|
||||
fetchCopilotToken,
|
||||
buildCopilotHeaders,
|
||||
} from "@api/copilot/token";
|
||||
} from "@api/copilot/auth/token";
|
||||
|
||||
export {
|
||||
requestDeviceCode,
|
||||
requestAccessToken,
|
||||
} from "@api/copilot/auth";
|
||||
} from "@api/copilot/auth/auth";
|
||||
|
||||
export { fetchModels } from "@api/copilot/models";
|
||||
export { fetchModels } from "@api/copilot/core/models";
|
||||
|
||||
export {
|
||||
getEndpoint,
|
||||
buildRequestBody,
|
||||
executeChatRequest,
|
||||
executeStreamRequest,
|
||||
} from "@api/copilot/chat";
|
||||
} from "@api/copilot/core/chat";
|
||||
|
||||
6
src/api/ollama/core/index.ts
Normal file
6
src/api/ollama/core/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Ollama API Core - Chat and Models API
|
||||
*/
|
||||
|
||||
export * from "./chat";
|
||||
export * from "./models";
|
||||
@@ -5,9 +5,9 @@
|
||||
export {
|
||||
executeChatRequest,
|
||||
executeStreamRequest,
|
||||
} from "@api/ollama/chat";
|
||||
} from "@api/ollama/core/chat";
|
||||
|
||||
export {
|
||||
fetchModels,
|
||||
checkHealth,
|
||||
} from "@api/ollama/models";
|
||||
} from "@api/ollama/core/models";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { appStore } from "@tui/index.ts";
|
||||
import { isQuietTool } from "@utils/tools.ts";
|
||||
import { isQuietTool } from "@utils/core/tools";
|
||||
import type { ToolCallParams } from "@interfaces/ToolCallParams.ts";
|
||||
|
||||
export const onToolCall = (call: ToolCallParams): void => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import { errorMessage, infoMessage, warningMessage } from "@utils/terminal";
|
||||
import { errorMessage, infoMessage, warningMessage } from "@utils/core/terminal";
|
||||
import { agentLoader } from "@services/agent-loader";
|
||||
import type { ChatState } from "@commands/components/chat/state";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { saveSession } from "@services/session";
|
||||
import { saveSession } from "@services/core/session";
|
||||
import { clearConversation } from "@commands/components/chat/history/clear-conversation";
|
||||
import { appStore } from "@tui/index";
|
||||
import { showContextFiles } from "@commands/components/chat/context/show-context-files";
|
||||
@@ -18,7 +18,7 @@ import { switchAgent } from "@commands/components/chat/agents/switch-agent";
|
||||
import { handleMCP } from "@commands/components/chat/mcp/handle-mcp";
|
||||
import { CommandContext } from "@interfaces/commandContext";
|
||||
import type { CommandHandler } from "@/types/commandHandler";
|
||||
import { successMessage } from "@utils/terminal";
|
||||
import { successMessage } from "@utils/core/terminal";
|
||||
|
||||
const COMMAND_REGISTRY: Map<string, CommandHandler> = new Map<
|
||||
string,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { warningMessage, infoMessage } from "@utils/terminal";
|
||||
import { warningMessage, infoMessage } from "@utils/core/terminal";
|
||||
import type { ChatState } from "@commands/components/chat/state";
|
||||
import COMMAND_REGISTRY from "@commands/components/chat/commands/commandsRegistry";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { resolve } from "path";
|
||||
import { existsSync } from "fs";
|
||||
import fg from "fast-glob";
|
||||
import { errorMessage, warningMessage } from "@utils/terminal";
|
||||
import { errorMessage, warningMessage } from "@utils/core/terminal";
|
||||
import { loadFile } from "@commands/components/chat/context/load-file";
|
||||
import { IGNORE_FOLDERS } from "@constants/paths";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readFile, stat } from "fs/promises";
|
||||
import { basename } from "path";
|
||||
import { warningMessage, successMessage, errorMessage } from "@utils/terminal";
|
||||
import { addContextFile } from "@services/session";
|
||||
import { warningMessage, successMessage, errorMessage } from "@utils/core/terminal";
|
||||
import { addContextFile } from "@services/core/session";
|
||||
|
||||
export const loadFile = async (
|
||||
filePath: string,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { basename } from "path";
|
||||
import { warningMessage, successMessage } from "@utils/terminal";
|
||||
import { warningMessage, successMessage } from "@utils/core/terminal";
|
||||
|
||||
export const removeFile = (
|
||||
filename: string,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import chalk from "chalk";
|
||||
import { basename } from "path";
|
||||
import { getCurrentSession } from "@services/session";
|
||||
import { infoMessage, filePath } from "@utils/terminal";
|
||||
import { getCurrentSession } from "@services/core/session";
|
||||
import { infoMessage, filePath } from "@utils/core/terminal";
|
||||
|
||||
export const showContextFiles = (contextFiles: Map<string, string>): void => {
|
||||
const session = getCurrentSession();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { clearMessages } from "@services/session";
|
||||
import { successMessage } from "@utils/terminal";
|
||||
import { clearMessages } from "@services/core/session";
|
||||
import { successMessage } from "@utils/core/terminal";
|
||||
import type { ChatState } from "@commands/components/chat/state";
|
||||
|
||||
export const clearConversation = (state: ChatState): void => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { successMessage, infoMessage } from "@utils/terminal";
|
||||
import { successMessage, infoMessage } from "@utils/core/terminal";
|
||||
import type { ChatState } from "@commands/components/chat/state";
|
||||
|
||||
export const compactHistory = (state: ChatState): void => {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import chalk from "chalk";
|
||||
import { infoMessage, errorMessage, warningMessage } from "@utils/terminal";
|
||||
import { infoMessage, errorMessage, warningMessage } from "@utils/core/terminal";
|
||||
import {
|
||||
createSession,
|
||||
loadSession,
|
||||
getMostRecentSession,
|
||||
findSession,
|
||||
setWorkingDirectory,
|
||||
} from "@services/session";
|
||||
import { getConfig } from "@services/config";
|
||||
} from "@services/core/session";
|
||||
import { getConfig } from "@services/core/config";
|
||||
import type { Provider as ProviderName, ChatSession } from "@/types/index";
|
||||
import { getProvider, getProviderStatus } from "@providers/index.ts";
|
||||
import {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import chalk from "chalk";
|
||||
import { basename, extname } from "path";
|
||||
import { addMessage } from "@services/session";
|
||||
import { initializePermissions } from "@services/permissions";
|
||||
import { createAgent } from "@services/agent";
|
||||
import { infoMessage, errorMessage, warningMessage } from "@utils/terminal";
|
||||
import { addMessage } from "@services/core/session";
|
||||
import { initializePermissions } from "@services/core/permissions";
|
||||
import { createAgent } from "@services/core/agent";
|
||||
import { infoMessage, errorMessage, warningMessage } from "@utils/core/terminal";
|
||||
import { getThinkingMessage } from "@constants/status-messages";
|
||||
import {
|
||||
detectDebuggingRequest,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getConfig } from "@services/config";
|
||||
import { getConfig } from "@services/core/config";
|
||||
import { displayProvidersStatus } from "@providers/index.ts";
|
||||
|
||||
export const showProviders = async (): Promise<void> => {
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
warningMessage,
|
||||
successMessage,
|
||||
errorMessage,
|
||||
} from "@utils/terminal";
|
||||
import { getConfig } from "@services/config";
|
||||
} from "@utils/core/terminal";
|
||||
import { getConfig } from "@services/core/config";
|
||||
import { getProvider } from "@providers/index.ts";
|
||||
import { showModels } from "./show-models.ts";
|
||||
import type { ChatState } from "../state.ts";
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
warningMessage,
|
||||
infoMessage,
|
||||
successMessage,
|
||||
} from "@utils/terminal";
|
||||
import { getConfig } from "@services/config";
|
||||
} from "@utils/core/terminal";
|
||||
import { getConfig } from "@services/core/config";
|
||||
import {
|
||||
getProvider,
|
||||
getProviderStatus,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import chalk from "chalk";
|
||||
import { basename, extname } from "path";
|
||||
import { initializePermissions } from "@services/permissions";
|
||||
import { createAgent } from "@services/agent";
|
||||
import { initializePermissions } from "@services/core/permissions";
|
||||
import { createAgent } from "@services/core/agent";
|
||||
import type { ChatState } from "@commands/components/chat/state";
|
||||
import { processFileReferences } from "@commands/components/chat/context/process-file-references";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import chalk from "chalk";
|
||||
import { getSessionSummaries } from "@services/session";
|
||||
import { infoMessage } from "@utils/terminal";
|
||||
import { getSessionSummaries } from "@services/core/session";
|
||||
import { infoMessage } from "@utils/core/terminal";
|
||||
|
||||
export const listSessions = async (): Promise<void> => {
|
||||
const summaries = await getSessionSummaries();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import chalk from "chalk";
|
||||
import { getCurrentSession } from "@services/session";
|
||||
import { warningMessage } from "@utils/terminal";
|
||||
import { getCurrentSession } from "@services/core/session";
|
||||
import { warningMessage } from "@utils/core/terminal";
|
||||
|
||||
export const showSessionInfo = async (): Promise<void> => {
|
||||
const session = getCurrentSession();
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import { usageStore } from "@stores/usage-store";
|
||||
import { getUserInfo } from "@providers/copilot/credentials";
|
||||
import { usageStore } from "@stores/core/usage-store";
|
||||
import { getUserInfo } from "@providers/copilot/auth/credentials";
|
||||
import { getCopilotUsage } from "@providers/copilot/usage";
|
||||
import { getProvider } from "@providers/index";
|
||||
import { renderUsageBar, renderUnlimitedBar } from "@utils/progress-bar";
|
||||
import { renderUsageBar, renderUnlimitedBar } from "@utils/menu/progress-bar";
|
||||
import type { ChatState } from "@commands/components/chat/state";
|
||||
import type { CopilotQuotaDetail } from "@/types/copilot-usage";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import os from "os";
|
||||
import { getConfig } from "@services/config";
|
||||
import { getConfig } from "@services/core/config";
|
||||
import { DASHBOARD_TITLE } from "@constants/dashboard";
|
||||
import type { DashboardConfig } from "@/types/dashboard";
|
||||
|
||||
|
||||
@@ -13,14 +13,14 @@ import {
|
||||
import type { ChatServiceState } from "@services/chat-tui-service";
|
||||
import type { ChatTUIOptions } from "@interfaces/ChatTUIOptions";
|
||||
import type { AgentConfig } from "@/types/agent-config";
|
||||
import { getConfig } from "@services/config";
|
||||
import { getConfig } from "@services/core/config";
|
||||
import { getThinkingMessage } from "@constants/status-messages";
|
||||
import {
|
||||
enterFullscreen,
|
||||
registerExitHandlers,
|
||||
exitFullscreen,
|
||||
clearScreen,
|
||||
} from "@utils/terminal";
|
||||
} from "@utils/core/terminal";
|
||||
import { createCallbacks } from "@commands/chat-tui";
|
||||
import { agentLoader } from "@services/agent-loader";
|
||||
import { projectSetupService } from "@services/project-setup-service";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Command handlers - Route commands to appropriate implementations
|
||||
*/
|
||||
|
||||
import { errorMessage } from "@utils/terminal";
|
||||
import { errorMessage } from "@utils/core/terminal";
|
||||
import { COMMAND_REGISTRY, isValidCommand } from "@commands/handlers/registry";
|
||||
import type { CommandOptions } from "@/types/index";
|
||||
|
||||
6
src/commands/core/index.ts
Normal file
6
src/commands/core/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Commands Core - Main command handling
|
||||
*/
|
||||
|
||||
export * from "./runner";
|
||||
export * from "./handlers";
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
errorMessage,
|
||||
failSpinner,
|
||||
headerMessage,
|
||||
} from "@utils/terminal";
|
||||
} from "@utils/core/terminal";
|
||||
import {
|
||||
INTENT_KEYWORDS,
|
||||
CLASSIFICATION_CONFIDENCE,
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
hightLigthedJson,
|
||||
headerMessage,
|
||||
infoMessage,
|
||||
} from "@utils/terminal";
|
||||
import { getConfig } from "@services/config";
|
||||
} from "@utils/core/terminal";
|
||||
import { getConfig } from "@services/core/config";
|
||||
import {
|
||||
VALID_CONFIG_KEYS,
|
||||
VALID_PROVIDERS,
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
startSpinner,
|
||||
succeedSpinner,
|
||||
successMessage,
|
||||
} from "@utils/terminal";
|
||||
} from "@utils/core/terminal";
|
||||
import type { CommandOptions } from "@/types/index";
|
||||
|
||||
export const handlePlan = async (options: CommandOptions): Promise<void> => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Run command handler
|
||||
*/
|
||||
|
||||
import { execute } from "@commands/runner";
|
||||
import { execute } from "@commands/core/runner";
|
||||
import type { CommandOptions } from "@/types/index";
|
||||
|
||||
export const handleRun = async (options: CommandOptions): Promise<void> => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Serve command handler
|
||||
*/
|
||||
|
||||
import { boxMessage, warningMessage, infoMessage } from "@utils/terminal";
|
||||
import { boxMessage, warningMessage, infoMessage } from "@utils/core/terminal";
|
||||
import type { CommandOptions } from "@/types/index";
|
||||
import { SERVER_INFO } from "@constants/serve";
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
errorMessage,
|
||||
headerMessage,
|
||||
filePath,
|
||||
} from "@utils/terminal";
|
||||
import { getConfig } from "@services/config";
|
||||
} from "@utils/core/terminal";
|
||||
import { getConfig } from "@services/core/config";
|
||||
import type { CommandOptions } from "@/types/index";
|
||||
|
||||
export const handleValidate = async (
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import { errorMessage, infoMessage, successMessage } from "@utils/terminal";
|
||||
import { errorMessage, infoMessage, successMessage } from "@utils/core/terminal";
|
||||
import {
|
||||
initializeMCP,
|
||||
getMCPConfig,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import { headerMessage, filePath } from "@utils/terminal";
|
||||
import { headerMessage, filePath } from "@utils/core/terminal";
|
||||
import type { RunnerOptions } from "@/types/runner";
|
||||
|
||||
export const displayHeader = (options: RunnerOptions): void => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import { filePath } from "@utils/terminal";
|
||||
import { filePath } from "@utils/core/terminal";
|
||||
import { STEP_ICONS, DEFAULT_STEP_ICON } from "@constants/runner";
|
||||
import type { ExecutionPlan, PlanStep } from "@/types/index";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Plan execution utilities
|
||||
*/
|
||||
|
||||
import { failSpinner, succeedSpinner, startSpinner } from "@utils/terminal";
|
||||
import { failSpinner, succeedSpinner, startSpinner } from "@utils/core/terminal";
|
||||
import { RUNNER_DELAYS } from "@constants/runner";
|
||||
import { getStepIcon } from "@commands/runner/display-plan";
|
||||
import { delay } from "@commands/runner/utils";
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
infoMessage,
|
||||
errorMessage,
|
||||
warningMessage,
|
||||
} from "@utils/terminal";
|
||||
} from "@utils/core/terminal";
|
||||
import { RUNNER_DELAYS, RUNNER_MESSAGES } from "@constants/runner";
|
||||
import { displayHeader } from "@commands/runner/display-header";
|
||||
import { displayPlan } from "@commands/runner/display-plan";
|
||||
|
||||
112
src/constants/multi-agent.ts
Normal file
112
src/constants/multi-agent.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Multi-Agent Constants
|
||||
*
|
||||
* Configuration defaults, messages, and descriptions for multi-agent execution.
|
||||
*/
|
||||
|
||||
import type { AgentExecutionMode, ConflictStrategy } from "@/types/multi-agent";
|
||||
|
||||
/**
|
||||
* Default configuration values
|
||||
*/
|
||||
export const MULTI_AGENT_DEFAULTS = {
|
||||
maxConcurrent: 3,
|
||||
timeout: 300000, // 5 minutes
|
||||
executionMode: "adaptive" as AgentExecutionMode,
|
||||
conflictStrategy: "serialize" as ConflictStrategy,
|
||||
abortOnFirstError: false,
|
||||
conflictCheckInterval: 1000, // 1 second
|
||||
maxRetries: 2,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Resource limits
|
||||
*/
|
||||
export const MULTI_AGENT_LIMITS = {
|
||||
maxAgentsPerRequest: 10,
|
||||
maxConcurrentRequests: 3,
|
||||
maxQueuedAgents: 20,
|
||||
maxConflictsBeforeAbort: 5,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Execution mode descriptions
|
||||
*/
|
||||
export const EXECUTION_MODE_DESCRIPTIONS: Record<AgentExecutionMode, string> = {
|
||||
sequential: "Execute agents one after another, safest for file modifications",
|
||||
parallel: "Execute all agents concurrently, fastest but may cause conflicts",
|
||||
adaptive: "Start parallel, automatically serialize when conflicts detected",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Conflict strategy descriptions
|
||||
*/
|
||||
export const CONFLICT_STRATEGY_DESCRIPTIONS: Record<ConflictStrategy, string> = {
|
||||
serialize: "Wait for conflicting agent to complete before proceeding",
|
||||
"abort-newer": "Abort the agent that started later when conflict detected",
|
||||
"merge-results": "Attempt to merge changes from both agents",
|
||||
isolated: "Each agent works in isolated context, merge at end",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Error messages
|
||||
*/
|
||||
export const MULTI_AGENT_ERRORS = {
|
||||
MAX_AGENTS_EXCEEDED: (max: number) =>
|
||||
`Cannot spawn more than ${max} agents in a single request`,
|
||||
MAX_CONCURRENT_EXCEEDED: (max: number) =>
|
||||
`Maximum concurrent agents (${max}) reached`,
|
||||
AGENT_NOT_FOUND: (name: string) =>
|
||||
`Agent "${name}" not found in registry`,
|
||||
AGENT_ALREADY_RUNNING: (id: string) =>
|
||||
`Agent "${id}" is already running`,
|
||||
EXECUTION_TIMEOUT: (agentId: string, timeout: number) =>
|
||||
`Agent "${agentId}" timed out after ${timeout}ms`,
|
||||
CONFLICT_RESOLUTION_FAILED: (filePath: string) =>
|
||||
`Failed to resolve conflict for file: ${filePath}`,
|
||||
EXECUTION_ABORTED: "Execution aborted by user",
|
||||
INVALID_EXECUTION_MODE: (mode: string) =>
|
||||
`Invalid execution mode: ${mode}`,
|
||||
INVALID_CONFLICT_STRATEGY: (strategy: string) =>
|
||||
`Invalid conflict strategy: ${strategy}`,
|
||||
TOO_MANY_CONFLICTS: (count: number) =>
|
||||
`Too many conflicts detected (${count}), aborting execution`,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Status messages
|
||||
*/
|
||||
export const MULTI_AGENT_MESSAGES = {
|
||||
STARTING: "Starting multi-agent execution",
|
||||
AGENT_SPAWNED: (name: string) => `Spawned agent: ${name}`,
|
||||
AGENT_COMPLETED: (name: string) => `Agent completed: ${name}`,
|
||||
AGENT_FAILED: (name: string, error: string) =>
|
||||
`Agent failed: ${name} - ${error}`,
|
||||
CONFLICT_DETECTED: (file: string, agents: string[]) =>
|
||||
`Conflict detected on ${file} between agents: ${agents.join(", ")}`,
|
||||
CONFLICT_RESOLVED: (file: string, strategy: string) =>
|
||||
`Conflict resolved for ${file} using ${strategy}`,
|
||||
EXECUTION_COMPLETE: (success: number, failed: number) =>
|
||||
`Execution complete: ${success} succeeded, ${failed} failed`,
|
||||
WAITING_FOR_CONFLICT: (agentId: string) =>
|
||||
`Agent ${agentId} waiting for conflict resolution`,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Agent ID generation prefix
|
||||
*/
|
||||
export const AGENT_ID_PREFIX = "agent_";
|
||||
|
||||
/**
|
||||
* Request ID generation prefix
|
||||
*/
|
||||
export const REQUEST_ID_PREFIX = "req_";
|
||||
|
||||
/**
|
||||
* File lock constants
|
||||
*/
|
||||
export const FILE_LOCK = {
|
||||
acquireTimeout: 5000, // 5 seconds
|
||||
retryInterval: 100, // 100ms
|
||||
maxRetries: 50,
|
||||
} as const;
|
||||
10
src/index.ts
10
src/index.ts
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Command } from "commander";
|
||||
import { handleCommand } from "@commands/handlers";
|
||||
import { handleCommand } from "@commands/core/handlers";
|
||||
import { execute } from "@commands/chat-tui";
|
||||
import versionData from "@/version.json";
|
||||
import {
|
||||
@@ -10,14 +10,14 @@ import {
|
||||
getProviderNames,
|
||||
displayProvidersStatus,
|
||||
} from "@providers/index";
|
||||
import { getConfig } from "@services/config";
|
||||
import { deleteSession, getSessionSummaries } from "@services/session";
|
||||
import { getConfig } from "@services/core/config";
|
||||
import { deleteSession, getSessionSummaries } from "@services/core/session";
|
||||
import {
|
||||
initializePermissions,
|
||||
listPatterns,
|
||||
addGlobalPattern,
|
||||
addLocalPattern,
|
||||
} from "@services/permissions";
|
||||
} from "@services/core/permissions";
|
||||
import {
|
||||
projectConfig,
|
||||
initProject,
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
buildLearningsContext,
|
||||
} from "@services/project-config";
|
||||
import { createPlan, displayPlan, approvePlan } from "@services/planner";
|
||||
import { ensureXdgDirectories } from "@utils/ensure-directories";
|
||||
import { ensureXdgDirectories } from "@utils/core/ensure-directories";
|
||||
import chalk from "chalk";
|
||||
|
||||
// Read version from version.json
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* Falls back to copilot.lua/copilot.vim config if available
|
||||
*/
|
||||
|
||||
import { chat, chatStream } from "@providers/copilot/chat";
|
||||
import { getModels, getDefaultModel } from "@providers/copilot/models";
|
||||
import { chat, chatStream } from "@providers/copilot/core/chat";
|
||||
import { getModels, getDefaultModel } from "@providers/copilot/core/models";
|
||||
import {
|
||||
getCredentials,
|
||||
setCredentials,
|
||||
@@ -13,11 +13,11 @@ import {
|
||||
logout,
|
||||
isConfigured,
|
||||
validate,
|
||||
} from "@providers/copilot/credentials";
|
||||
} from "@providers/copilot/auth/credentials";
|
||||
import {
|
||||
initiateDeviceFlow,
|
||||
pollForAccessToken,
|
||||
} from "@providers/copilot/auth";
|
||||
} from "@providers/copilot/auth/auth";
|
||||
import {
|
||||
COPILOT_PROVIDER_NAME,
|
||||
COPILOT_DISPLAY_NAME,
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
setLoggedOut,
|
||||
clearCredentials,
|
||||
} from "@providers/copilot/state";
|
||||
import { getOAuthToken } from "@providers/copilot/token";
|
||||
import { getOAuthToken } from "@providers/copilot/auth/token";
|
||||
import type { ProviderCredentials } from "@/types/providers";
|
||||
import type { CopilotUserInfo } from "@/types/copilot";
|
||||
|
||||
@@ -70,7 +70,7 @@ export const validate = async (): Promise<{
|
||||
error?: string;
|
||||
}> => {
|
||||
try {
|
||||
const { refreshToken } = await import("@providers/copilot/token");
|
||||
const { refreshToken } = await import("@providers/copilot/auth/token");
|
||||
const token = await refreshToken();
|
||||
return { valid: !!token.token };
|
||||
} catch (error) {
|
||||
7
src/providers/copilot/auth/index.ts
Normal file
7
src/providers/copilot/auth/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copilot Provider Auth - Authentication and Credentials
|
||||
*/
|
||||
|
||||
export * from "./auth";
|
||||
export * from "./token";
|
||||
export * from "./credentials";
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
COPILOT_MAX_RETRIES,
|
||||
COPILOT_UNLIMITED_MODEL,
|
||||
} from "@constants/copilot";
|
||||
import { refreshToken, buildHeaders } from "@providers/copilot/token";
|
||||
import { getDefaultModel, isModelUnlimited } from "@providers/copilot/models";
|
||||
import { refreshToken, buildHeaders } from "@providers/copilot/auth/token";
|
||||
import { getDefaultModel, isModelUnlimited } from "@providers/copilot/core/models";
|
||||
import {
|
||||
sleep,
|
||||
isRateLimitError,
|
||||
@@ -23,7 +23,7 @@ import type {
|
||||
ChatCompletionResponse,
|
||||
StreamChunk,
|
||||
} from "@/types/providers";
|
||||
import { addDebugLog } from "@tui-solid/components/debug-log-panel";
|
||||
import { addDebugLog } from "@tui-solid/components/logs/debug-log-panel";
|
||||
|
||||
interface FormattedMessage {
|
||||
role: string;
|
||||
6
src/providers/copilot/core/index.ts
Normal file
6
src/providers/copilot/core/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Copilot Provider Core - Chat and Models
|
||||
*/
|
||||
|
||||
export * from "./chat";
|
||||
export * from "./models";
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
COPILOT_UNLIMITED_MODEL,
|
||||
} from "@constants/copilot";
|
||||
import { getState, setModels } from "@providers/copilot/state";
|
||||
import { refreshToken } from "@providers/copilot/token";
|
||||
import { refreshToken } from "@providers/copilot/auth/token";
|
||||
import type { ProviderModel } from "@/types/providers";
|
||||
|
||||
interface ModelBilling {
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import got from "got";
|
||||
|
||||
import { getOAuthToken } from "@providers/copilot/token";
|
||||
import { getOAuthToken } from "@providers/copilot/auth/token";
|
||||
import type { CopilotUsageResponse } from "@/types/copilot-usage";
|
||||
|
||||
const COPILOT_USER_URL = "https://api.github.com/copilot_internal/user";
|
||||
|
||||
18
src/providers/copilot/user-info.ts
Normal file
18
src/providers/copilot/user-info.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copilot User Info Wrapper
|
||||
*
|
||||
* Provides a consistent interface for getting Copilot user information.
|
||||
*/
|
||||
|
||||
import { getUserInfo } from "@providers/copilot/auth/credentials";
|
||||
|
||||
/**
|
||||
* Get Copilot user info with consistent interface
|
||||
*/
|
||||
export const getCopilotUserInfo = async (): Promise<{
|
||||
login: string;
|
||||
name?: string;
|
||||
email?: string;
|
||||
} | null> => {
|
||||
return getUserInfo();
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
* Provider chat functions
|
||||
*/
|
||||
|
||||
import { getProvider } from "@providers/registry";
|
||||
import { getProvider } from "@providers/core/registry";
|
||||
import type {
|
||||
ProviderName,
|
||||
Message,
|
||||
8
src/providers/core/index.ts
Normal file
8
src/providers/core/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Providers Core - Common provider functionality
|
||||
*/
|
||||
|
||||
export * from "./registry";
|
||||
export * from "./chat";
|
||||
export * from "./credentials";
|
||||
export * from "./status";
|
||||
@@ -5,7 +5,7 @@
|
||||
import chalk from "chalk";
|
||||
|
||||
import { PROVIDER_INFO } from "@constants/providers";
|
||||
import { getProvider, getProviderNames } from "@providers/registry";
|
||||
import { getProvider, getProviderNames } from "@providers/core/registry";
|
||||
import type { ProviderName, ProviderStatus } from "@/types/providers";
|
||||
|
||||
export const getProviderStatus = async (
|
||||
@@ -12,10 +12,10 @@ export {
|
||||
getAllProviders,
|
||||
getProviderNames,
|
||||
isValidProvider,
|
||||
} from "@providers/registry";
|
||||
} from "@providers/core/registry";
|
||||
|
||||
// Re-export status functions
|
||||
export { getProviderStatus, displayProvidersStatus } from "@providers/status";
|
||||
export { getProviderStatus, displayProvidersStatus } from "@providers/core/status";
|
||||
|
||||
// Re-export login functions
|
||||
export {
|
||||
@@ -26,7 +26,7 @@ export {
|
||||
} from "@providers/login";
|
||||
|
||||
// Re-export chat functions
|
||||
export { chat, chatStream, getDefaultModel, getModels } from "@providers/chat";
|
||||
export { chat, chatStream, getDefaultModel, getModels } from "@providers/core/chat";
|
||||
|
||||
// Re-export copilot-specific functions
|
||||
export {
|
||||
|
||||
@@ -6,7 +6,7 @@ export { loginProvider, logoutProvider } from "@providers/login/handlers";
|
||||
export {
|
||||
initializeProviders,
|
||||
completeCopilotLogin,
|
||||
} from "@providers/login/initialize";
|
||||
} from "@providers/login/core/initialize";
|
||||
export { displayModels } from "@providers/login/utils";
|
||||
export { loginCopilot } from "@providers/login/copilot-login";
|
||||
export { loginOllama } from "@providers/login/ollama-login";
|
||||
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
LOGIN_PROMPTS,
|
||||
AUTH_STEP_PREFIXES,
|
||||
} from "@constants/login";
|
||||
import { getProvider } from "@providers/registry";
|
||||
import { getProviderStatus } from "@providers/status";
|
||||
import { loadCredentials, saveCredentials } from "@providers/credentials";
|
||||
import { getProvider } from "@providers/core/registry";
|
||||
import { getProviderStatus } from "@providers/core/status";
|
||||
import { loadCredentials, saveCredentials } from "@providers/core/credentials";
|
||||
import { initiateDeviceFlow, pollForAccessToken } from "@providers/copilot";
|
||||
import { displayModels } from "@providers/login/utils";
|
||||
import type { ProviderName, LoginHandler } from "@/types/providers";
|
||||
|
||||
5
src/providers/login/core/index.ts
Normal file
5
src/providers/login/core/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Login Core - Initialization
|
||||
*/
|
||||
|
||||
export * from "./initialize";
|
||||
@@ -2,8 +2,8 @@
|
||||
* Provider initialization
|
||||
*/
|
||||
|
||||
import { getProvider, isValidProvider } from "@providers/registry";
|
||||
import { loadCredentials, saveCredentials } from "@providers/credentials";
|
||||
import { getProvider, isValidProvider } from "@providers/core/registry";
|
||||
import { loadCredentials, saveCredentials } from "@providers/core/credentials";
|
||||
import { getLogoutHandler } from "@providers/login/handlers";
|
||||
import type { ProviderName } from "@/types/providers";
|
||||
|
||||
@@ -6,8 +6,8 @@ import chalk from "chalk";
|
||||
|
||||
import { PROVIDER_INFO } from "@constants/providers";
|
||||
import { LOGIN_MESSAGES } from "@constants/login";
|
||||
import { getProvider } from "@providers/registry";
|
||||
import { loadCredentials, saveCredentials } from "@providers/credentials";
|
||||
import { getProvider } from "@providers/core/registry";
|
||||
import { loadCredentials, saveCredentials } from "@providers/core/credentials";
|
||||
import { logoutCopilot } from "@providers/copilot";
|
||||
import { loginCopilot } from "@providers/login/copilot-login";
|
||||
import { loginOllama } from "@providers/login/ollama-login";
|
||||
|
||||
@@ -7,8 +7,8 @@ import chalk from "chalk";
|
||||
|
||||
import { DEFAULT_OLLAMA_HOST } from "@constants/providers";
|
||||
import { LOGIN_MESSAGES, LOGIN_PROMPTS } from "@constants/login";
|
||||
import { getProvider } from "@providers/registry";
|
||||
import { loadCredentials, saveCredentials } from "@providers/credentials";
|
||||
import { getProvider } from "@providers/core/registry";
|
||||
import { loadCredentials, saveCredentials } from "@providers/core/credentials";
|
||||
import { displayModels } from "@providers/login/utils";
|
||||
import type { ProviderName, LoginHandler } from "@/types/providers";
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
import {
|
||||
getOllamaModels,
|
||||
getDefaultOllamaModel,
|
||||
} from "@providers/ollama/models";
|
||||
import { ollamaChat } from "@providers/ollama/chat";
|
||||
} from "@providers/ollama/core/models";
|
||||
import { ollamaChat } from "@providers/ollama/core/chat";
|
||||
import { ollamaChatStream } from "@providers/ollama/stream";
|
||||
import {
|
||||
getOllamaCredentials,
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
OLLAMA_CHAT_OPTIONS,
|
||||
} from "@constants/ollama";
|
||||
import { getOllamaBaseUrl } from "@providers/ollama/state";
|
||||
import { getDefaultOllamaModel } from "@providers/ollama/models";
|
||||
import { getDefaultOllamaModel } from "@providers/ollama/core/models";
|
||||
import type {
|
||||
Message,
|
||||
ChatCompletionOptions,
|
||||
6
src/providers/ollama/core/index.ts
Normal file
6
src/providers/ollama/core/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Ollama Provider Core - Chat and Models
|
||||
*/
|
||||
|
||||
export * from "./chat";
|
||||
export * from "./models";
|
||||
@@ -6,14 +6,14 @@ import got from "got";
|
||||
|
||||
import { OLLAMA_ENDPOINTS, OLLAMA_TIMEOUTS } from "@constants/ollama";
|
||||
import { getOllamaBaseUrl } from "@providers/ollama/state";
|
||||
import { buildChatRequest, mapToolCall } from "@providers/ollama/chat";
|
||||
import { buildChatRequest, mapToolCall } from "@providers/ollama/core/chat";
|
||||
import type {
|
||||
Message,
|
||||
ChatCompletionOptions,
|
||||
StreamChunk,
|
||||
} from "@/types/providers";
|
||||
import type { OllamaChatResponse } from "@/types/ollama";
|
||||
import { addDebugLog } from "@tui-solid/components/debug-log-panel";
|
||||
import { addDebugLog } from "@tui-solid/components/logs/debug-log-panel";
|
||||
|
||||
const parseStreamLine = (
|
||||
line: string,
|
||||
|
||||
@@ -20,9 +20,9 @@ import type {
|
||||
PartialToolCall,
|
||||
StreamCallbacks,
|
||||
} from "@/types/streaming";
|
||||
import { chatStream } from "@providers/chat";
|
||||
import { chatStream } from "@providers/core/chat";
|
||||
import { getTool, getToolsForApi, refreshMCPTools } from "@tools/index";
|
||||
import { initializePermissions } from "@services/permissions";
|
||||
import { initializePermissions } from "@services/core/permissions";
|
||||
import { MAX_ITERATIONS, MAX_CONSECUTIVE_ERRORS } from "@constants/agent";
|
||||
import { createStreamAccumulator } from "@/types/streaming";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Chat TUI command handling
|
||||
*/
|
||||
|
||||
import { saveSession as saveSessionSession } from "@services/session";
|
||||
import { saveSession as saveSessionSession } from "@services/core/session";
|
||||
import { appStore } from "@tui/index";
|
||||
import { CHAT_MESSAGES, type CommandName } from "@constants/chat-service";
|
||||
import { handleLogin, handleLogout, showWhoami } from "@services/chat-tui/auth";
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
* Chat TUI initialization
|
||||
*/
|
||||
|
||||
import { errorMessage, infoMessage } from "@utils/terminal";
|
||||
import { errorMessage, infoMessage } from "@utils/core/terminal";
|
||||
import {
|
||||
findSession,
|
||||
loadSession,
|
||||
createSession,
|
||||
getMostRecentSession,
|
||||
} from "@services/session";
|
||||
import { getConfig } from "@services/config";
|
||||
import { initializePermissions } from "@services/permissions";
|
||||
} from "@services/core/session";
|
||||
import { getConfig } from "@services/core/config";
|
||||
import { initializePermissions } from "@services/core/permissions";
|
||||
import { getProviderStatus } from "@providers/index";
|
||||
import { appStore } from "@tui/index";
|
||||
import { themeActions } from "@stores/theme-store";
|
||||
import { themeActions } from "@stores/core/theme-store";
|
||||
import {
|
||||
buildBaseContext,
|
||||
buildCompletePrompt,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Chat TUI message handling
|
||||
*/
|
||||
|
||||
import { addMessage, saveSession } from "@services/session";
|
||||
import { addMessage, saveSession } from "@services/core/session";
|
||||
import { createStreamingAgent } from "@services/agent-stream";
|
||||
import { CHAT_MESSAGES } from "@constants/chat-service";
|
||||
import { enrichMessageWithIssues } from "@services/github-issue-service";
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
checkOllamaAvailability,
|
||||
checkCopilotAvailability,
|
||||
} from "@services/cascading-provider";
|
||||
import { chat, getDefaultModel } from "@providers/chat";
|
||||
import { chat, getDefaultModel } from "@providers/core/chat";
|
||||
import { AUDIT_SYSTEM_PROMPT, createAuditPrompt, parseAuditResponse } from "@prompts/audit-prompt";
|
||||
import { PROVIDER_IDS } from "@constants/provider-quality";
|
||||
import { appStore } from "@tui/index";
|
||||
@@ -55,7 +55,7 @@ import type {
|
||||
ChatServiceCallbacks,
|
||||
ToolCallInfo,
|
||||
} from "@/types/chat-service";
|
||||
import { addDebugLog } from "@tui-solid/components/debug-log-panel";
|
||||
import { addDebugLog } from "@tui-solid/components/logs/debug-log-panel";
|
||||
import { FILE_MODIFYING_TOOLS } from "@constants/tools";
|
||||
import type { StreamCallbacksWithState } from "@interfaces/StreamCallbacksWithState";
|
||||
import {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { MODEL_MESSAGES } from "@constants/chat-service";
|
||||
import { getConfig } from "@services/config";
|
||||
import { getConfig } from "@services/core/config";
|
||||
import {
|
||||
getProvider,
|
||||
getDefaultModel,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { setPermissionHandler } from "@services/permissions";
|
||||
import { setPermissionHandler } from "@services/core/permissions";
|
||||
import type {
|
||||
PermissionPromptRequest,
|
||||
PermissionPromptResponse,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Chat TUI print mode (non-interactive)
|
||||
*/
|
||||
|
||||
import { createAgent } from "@services/agent";
|
||||
import { initializePermissions } from "@services/permissions";
|
||||
import { createAgent } from "@services/core/agent";
|
||||
import { initializePermissions } from "@services/core/permissions";
|
||||
import {
|
||||
processFileReferences,
|
||||
buildContextMessage,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Usage statistics display for TUI
|
||||
*/
|
||||
|
||||
import { usageStore } from "@stores/usage-store";
|
||||
import { getUserInfo } from "@providers/copilot/credentials";
|
||||
import { usageStore } from "@stores/core/usage-store";
|
||||
import { getUserInfo } from "@providers/copilot/auth/credentials";
|
||||
import { getCopilotUsage } from "@providers/copilot/usage";
|
||||
import { PROGRESS_BAR } from "@constants/ui";
|
||||
import type {
|
||||
|
||||
@@ -22,14 +22,14 @@ import type {
|
||||
import { chat as providerChat } from "@providers/index";
|
||||
import { getTool, getToolsForApi, refreshMCPTools } from "@tools/index";
|
||||
import type { ToolContext, ToolCall, ToolResult } from "@/types/tools";
|
||||
import { initializePermissions } from "@services/permissions";
|
||||
import { initializePermissions } from "@services/core/permissions";
|
||||
import {
|
||||
loadHooks,
|
||||
executePreToolUseHooks,
|
||||
executePostToolUseHooks,
|
||||
} from "@services/hooks-service";
|
||||
import { MAX_ITERATIONS } from "@constants/agent";
|
||||
import { usageStore } from "@stores/usage-store";
|
||||
import { usageStore } from "@stores/core/usage-store";
|
||||
|
||||
/**
|
||||
* Agent state interface
|
||||
@@ -7,7 +7,7 @@ import { promisify } from "util";
|
||||
import chalk from "chalk";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { promptBashPermission } from "@services/permissions";
|
||||
import { promptBashPermission } from "@services/core/permissions";
|
||||
import type { ExecutionResult } from "@interfaces/ExecutionResult";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
91
src/services/core/index.ts
Normal file
91
src/services/core/index.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Services Core - Core service exports
|
||||
*/
|
||||
|
||||
// Agent
|
||||
export {
|
||||
runAgentLoop,
|
||||
runAgent,
|
||||
createAgent,
|
||||
type AgentOptions,
|
||||
type AgentResult,
|
||||
} from "./agent";
|
||||
|
||||
// Permissions
|
||||
export {
|
||||
setWorkingDir as setPermissionsWorkingDir,
|
||||
setPermissionHandler,
|
||||
initializePermissions,
|
||||
parsePattern,
|
||||
matchesBashPattern,
|
||||
matchesPathPattern,
|
||||
isBashAllowed,
|
||||
isBashDenied,
|
||||
isFileOpAllowed,
|
||||
generateBashPattern,
|
||||
addSessionPattern,
|
||||
addGlobalPattern,
|
||||
addLocalPattern,
|
||||
listPatterns,
|
||||
clearSessionPatterns,
|
||||
promptBashPermission,
|
||||
promptFilePermission,
|
||||
promptPermission,
|
||||
getPermissionLevel,
|
||||
type ToolType,
|
||||
type PermissionPattern,
|
||||
type PermissionsConfig,
|
||||
type PermissionHandler,
|
||||
} from "./permissions";
|
||||
|
||||
// Session
|
||||
export {
|
||||
createSession,
|
||||
loadSession,
|
||||
saveSession,
|
||||
addMessage,
|
||||
addContextFile,
|
||||
removeContextFile,
|
||||
getCurrentSession,
|
||||
listSessions,
|
||||
deleteSession,
|
||||
clearMessages,
|
||||
getMostRecentSession,
|
||||
getSessionSummaries,
|
||||
findSession,
|
||||
setWorkingDirectory,
|
||||
type SessionInfo,
|
||||
} from "./session";
|
||||
|
||||
// Executor
|
||||
export {
|
||||
setWorkingDir as setExecutorWorkingDir,
|
||||
getWorkingDir,
|
||||
executeCommand,
|
||||
executeStreamingCommand,
|
||||
readFile,
|
||||
writeFile,
|
||||
editFile,
|
||||
deleteFile,
|
||||
createDirectory,
|
||||
listDirectory,
|
||||
pathExists,
|
||||
getStats,
|
||||
type ExecutionResult,
|
||||
type FileOperation,
|
||||
} from "./executor";
|
||||
|
||||
// Config
|
||||
export {
|
||||
loadConfig,
|
||||
saveConfig,
|
||||
getConfigValue,
|
||||
setConfigValue,
|
||||
getAllConfig,
|
||||
getApiKey,
|
||||
getModel,
|
||||
getConfigPath,
|
||||
isProtectedPath,
|
||||
resetConfig,
|
||||
getConfig,
|
||||
} from "./config";
|
||||
@@ -2,6 +2,7 @@
|
||||
* Services Module - Business logic extracted from UI components
|
||||
*/
|
||||
|
||||
// Feature services
|
||||
export * from "@services/file-picker-service";
|
||||
export * from "@services/chat-tui-service";
|
||||
export * from "@services/github-issue-service";
|
||||
@@ -9,3 +10,6 @@ export * from "@services/command-suggestion-service";
|
||||
export * from "@services/learning-service";
|
||||
export * from "@services/rules-service";
|
||||
export * as brainService from "@services/brain";
|
||||
|
||||
// Note: Core services (agent, permissions, session, executor, config) are imported
|
||||
// directly from @services/core/* to avoid naming conflicts with chat-tui-service
|
||||
|
||||
224
src/services/multi-agent/agent-manager.ts
Normal file
224
src/services/multi-agent/agent-manager.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* Agent Manager
|
||||
*
|
||||
* Manages agent instance lifecycle: creation, starting, stopping, and cleanup.
|
||||
*/
|
||||
|
||||
import type {
|
||||
AgentInstance,
|
||||
AgentSpawnConfig,
|
||||
AgentConversation,
|
||||
AgentExecutionResult,
|
||||
} from "@/types/multi-agent";
|
||||
import type { AgentDefinition } from "@/types/agent-definition";
|
||||
import { multiAgentStore } from "@stores/core/multi-agent-store";
|
||||
import {
|
||||
MULTI_AGENT_ERRORS,
|
||||
MULTI_AGENT_DEFAULTS,
|
||||
MULTI_AGENT_LIMITS,
|
||||
} from "@/constants/multi-agent";
|
||||
|
||||
/**
|
||||
* Agent registry cache
|
||||
*/
|
||||
let agentRegistry: Map<string, AgentDefinition> = new Map();
|
||||
|
||||
/**
|
||||
* Set the agent registry (called during initialization)
|
||||
*/
|
||||
export const setAgentRegistry = (registry: Map<string, AgentDefinition>): void => {
|
||||
agentRegistry = registry;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get agent definition by name
|
||||
*/
|
||||
export const getAgentDefinition = (name: string): AgentDefinition | undefined => {
|
||||
return agentRegistry.get(name);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an agent instance from config
|
||||
*/
|
||||
export const createAgentInstance = (
|
||||
config: AgentSpawnConfig,
|
||||
): AgentInstance | { error: string } => {
|
||||
const definition = getAgentDefinition(config.agentName);
|
||||
if (!definition) {
|
||||
return { error: MULTI_AGENT_ERRORS.AGENT_NOT_FOUND(config.agentName) };
|
||||
}
|
||||
|
||||
const activeCount = multiAgentStore.getActiveInstances().length;
|
||||
if (activeCount >= MULTI_AGENT_LIMITS.maxConcurrentRequests) {
|
||||
return { error: MULTI_AGENT_ERRORS.MAX_CONCURRENT_EXCEEDED(MULTI_AGENT_LIMITS.maxConcurrentRequests) };
|
||||
}
|
||||
|
||||
const conversation: AgentConversation = {
|
||||
messages: [],
|
||||
toolCalls: [],
|
||||
};
|
||||
|
||||
const instance: Omit<AgentInstance, "id"> = {
|
||||
definition,
|
||||
config,
|
||||
status: "pending",
|
||||
conversation,
|
||||
startedAt: Date.now(),
|
||||
modifiedFiles: [],
|
||||
};
|
||||
|
||||
const id = multiAgentStore.addInstance(instance);
|
||||
|
||||
return {
|
||||
...instance,
|
||||
id,
|
||||
} as AgentInstance;
|
||||
};
|
||||
|
||||
/**
|
||||
* Start an agent instance
|
||||
*/
|
||||
export const startAgent = (agentId: string): void => {
|
||||
multiAgentStore.updateInstanceStatus(agentId, "running");
|
||||
};
|
||||
|
||||
/**
|
||||
* Pause agent due to conflict
|
||||
*/
|
||||
export const pauseAgentForConflict = (agentId: string): void => {
|
||||
multiAgentStore.updateInstanceStatus(agentId, "waiting_conflict");
|
||||
};
|
||||
|
||||
/**
|
||||
* Resume agent after conflict resolution
|
||||
*/
|
||||
export const resumeAgent = (agentId: string): void => {
|
||||
const instance = multiAgentStore.getState().instances.get(agentId);
|
||||
if (instance?.status === "waiting_conflict") {
|
||||
multiAgentStore.updateInstanceStatus(agentId, "running");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Complete an agent with result
|
||||
*/
|
||||
export const completeAgent = (
|
||||
agentId: string,
|
||||
result: AgentExecutionResult,
|
||||
): void => {
|
||||
const state = multiAgentStore.getState();
|
||||
const instance = state.instances.get(agentId);
|
||||
if (!instance) return;
|
||||
|
||||
multiAgentStore.updateInstanceStatus(
|
||||
agentId,
|
||||
result.success ? "completed" : "error",
|
||||
result.error,
|
||||
);
|
||||
|
||||
multiAgentStore.addEvent({
|
||||
type: result.success ? "agent_completed" : "agent_error",
|
||||
agentId,
|
||||
...(result.success
|
||||
? { result, timestamp: Date.now() }
|
||||
: { error: result.error ?? "Unknown error", timestamp: Date.now() }),
|
||||
} as { type: "agent_completed"; agentId: string; result: AgentExecutionResult; timestamp: number } | { type: "agent_error"; agentId: string; error: string; timestamp: number });
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel an agent
|
||||
*/
|
||||
export const cancelAgent = (agentId: string, reason?: string): void => {
|
||||
multiAgentStore.updateInstanceStatus(
|
||||
agentId,
|
||||
"cancelled",
|
||||
reason ?? "Cancelled by user",
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get agent by ID
|
||||
*/
|
||||
export const getAgent = (agentId: string): AgentInstance | undefined => {
|
||||
return multiAgentStore.getState().instances.get(agentId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate spawn config
|
||||
*/
|
||||
export const validateSpawnConfig = (
|
||||
config: AgentSpawnConfig,
|
||||
): { valid: boolean; errors: string[] } => {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!config.agentName) {
|
||||
errors.push("Agent name is required");
|
||||
}
|
||||
|
||||
if (!config.task) {
|
||||
errors.push("Task is required");
|
||||
}
|
||||
|
||||
const definition = getAgentDefinition(config.agentName);
|
||||
if (!definition) {
|
||||
errors.push(MULTI_AGENT_ERRORS.AGENT_NOT_FOUND(config.agentName));
|
||||
}
|
||||
|
||||
if (config.timeout && config.timeout < 1000) {
|
||||
errors.push("Timeout must be at least 1000ms");
|
||||
}
|
||||
|
||||
if (config.timeout && config.timeout > MULTI_AGENT_DEFAULTS.timeout * 2) {
|
||||
errors.push(`Timeout cannot exceed ${MULTI_AGENT_DEFAULTS.timeout * 2}ms`);
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors };
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all running agents
|
||||
*/
|
||||
export const getRunningAgents = (): AgentInstance[] => {
|
||||
return multiAgentStore.getInstancesByStatus("running");
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all agents waiting on conflicts
|
||||
*/
|
||||
export const getWaitingAgents = (): AgentInstance[] => {
|
||||
return multiAgentStore.getInstancesByStatus("waiting_conflict");
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel all running agents
|
||||
*/
|
||||
export const cancelAllAgents = (reason?: string): void => {
|
||||
const running = getRunningAgents();
|
||||
const waiting = getWaitingAgents();
|
||||
|
||||
[...running, ...waiting].forEach((agent) => {
|
||||
cancelAgent(agent.id, reason ?? MULTI_AGENT_ERRORS.EXECUTION_ABORTED);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get agent statistics
|
||||
*/
|
||||
export const getAgentStats = (): {
|
||||
running: number;
|
||||
waiting: number;
|
||||
completed: number;
|
||||
failed: number;
|
||||
cancelled: number;
|
||||
} => {
|
||||
const state = multiAgentStore.getState();
|
||||
const instances = Array.from(state.instances.values());
|
||||
|
||||
return {
|
||||
running: instances.filter((i) => i.status === "running").length,
|
||||
waiting: instances.filter((i) => i.status === "waiting_conflict").length,
|
||||
completed: instances.filter((i) => i.status === "completed").length,
|
||||
failed: instances.filter((i) => i.status === "error").length,
|
||||
cancelled: instances.filter((i) => i.status === "cancelled").length,
|
||||
};
|
||||
};
|
||||
299
src/services/multi-agent/conflict-handler.ts
Normal file
299
src/services/multi-agent/conflict-handler.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* Conflict Handler
|
||||
*
|
||||
* Detects and resolves file conflicts between concurrent agents.
|
||||
*/
|
||||
|
||||
import type {
|
||||
FileConflict,
|
||||
ConflictStrategy,
|
||||
ConflictResolutionResult,
|
||||
AgentInstance,
|
||||
} from "@/types/multi-agent";
|
||||
import { multiAgentStore } from "@stores/core/multi-agent-store";
|
||||
import {
|
||||
MULTI_AGENT_ERRORS,
|
||||
FILE_LOCK,
|
||||
} from "@/constants/multi-agent";
|
||||
import {
|
||||
pauseAgentForConflict,
|
||||
resumeAgent,
|
||||
} from "@/services/multi-agent/agent-manager";
|
||||
|
||||
/**
|
||||
* File locks for tracking which agent owns which file
|
||||
*/
|
||||
const fileLocks: Map<string, string> = new Map(); // filePath -> agentId
|
||||
|
||||
/**
|
||||
* Pending lock requests
|
||||
*/
|
||||
const pendingLocks: Map<string, Array<{
|
||||
agentId: string;
|
||||
resolve: (acquired: boolean) => void;
|
||||
}>> = new Map();
|
||||
|
||||
/**
|
||||
* Acquire a file lock for an agent
|
||||
*/
|
||||
export const acquireFileLock = async (
|
||||
agentId: string,
|
||||
filePath: string,
|
||||
): Promise<boolean> => {
|
||||
const currentOwner = fileLocks.get(filePath);
|
||||
|
||||
if (!currentOwner) {
|
||||
fileLocks.set(filePath, agentId);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentOwner === agentId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// File is locked by another agent - add to pending queue
|
||||
return new Promise((resolve) => {
|
||||
const pending = pendingLocks.get(filePath) ?? [];
|
||||
pending.push({ agentId, resolve });
|
||||
pendingLocks.set(filePath, pending);
|
||||
|
||||
// Set timeout for lock acquisition
|
||||
setTimeout(() => {
|
||||
const queue = pendingLocks.get(filePath) ?? [];
|
||||
const idx = queue.findIndex((p) => p.agentId === agentId);
|
||||
if (idx !== -1) {
|
||||
queue.splice(idx, 1);
|
||||
pendingLocks.set(filePath, queue);
|
||||
resolve(false);
|
||||
}
|
||||
}, FILE_LOCK.acquireTimeout);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Release a file lock
|
||||
*/
|
||||
export const releaseFileLock = (agentId: string, filePath: string): void => {
|
||||
const currentOwner = fileLocks.get(filePath);
|
||||
if (currentOwner !== agentId) return;
|
||||
|
||||
fileLocks.delete(filePath);
|
||||
|
||||
// Grant lock to next pending agent
|
||||
const pending = pendingLocks.get(filePath) ?? [];
|
||||
if (pending.length > 0) {
|
||||
const next = pending.shift();
|
||||
if (next) {
|
||||
pendingLocks.set(filePath, pending);
|
||||
fileLocks.set(filePath, next.agentId);
|
||||
next.resolve(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Release all locks held by an agent
|
||||
*/
|
||||
export const releaseAllLocks = (agentId: string): void => {
|
||||
const locksToRelease: string[] = [];
|
||||
|
||||
fileLocks.forEach((owner, path) => {
|
||||
if (owner === agentId) {
|
||||
locksToRelease.push(path);
|
||||
}
|
||||
});
|
||||
|
||||
locksToRelease.forEach((path) => releaseFileLock(agentId, path));
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a file is locked
|
||||
*/
|
||||
export const isFileLocked = (filePath: string): boolean => {
|
||||
return fileLocks.has(filePath);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the agent that holds a file lock
|
||||
*/
|
||||
export const getFileLockOwner = (filePath: string): string | null => {
|
||||
return fileLocks.get(filePath) ?? null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect conflict when agent tries to modify a file
|
||||
*/
|
||||
export const detectConflict = (
|
||||
agentId: string,
|
||||
filePath: string,
|
||||
): FileConflict | null => {
|
||||
const currentOwner = fileLocks.get(filePath);
|
||||
|
||||
if (!currentOwner || currentOwner === agentId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const conflict: FileConflict = {
|
||||
filePath,
|
||||
conflictingAgentIds: [currentOwner, agentId],
|
||||
detectedAt: Date.now(),
|
||||
};
|
||||
|
||||
multiAgentStore.addConflict(conflict);
|
||||
return conflict;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve conflict using specified strategy
|
||||
*/
|
||||
export const resolveConflict = async (
|
||||
conflict: FileConflict,
|
||||
strategy: ConflictStrategy,
|
||||
): Promise<ConflictResolutionResult> => {
|
||||
const resolutionHandlers: Record<
|
||||
ConflictStrategy,
|
||||
() => Promise<ConflictResolutionResult>
|
||||
> = {
|
||||
serialize: () => handleSerializeStrategy(conflict),
|
||||
"abort-newer": () => handleAbortNewerStrategy(conflict),
|
||||
"merge-results": () => handleMergeStrategy(conflict),
|
||||
isolated: () => handleIsolatedStrategy(conflict),
|
||||
};
|
||||
|
||||
const result = await resolutionHandlers[strategy]();
|
||||
multiAgentStore.resolveConflict(conflict.filePath, result);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle serialize strategy - wait for owner to finish
|
||||
*/
|
||||
const handleSerializeStrategy = async (
|
||||
conflict: FileConflict,
|
||||
): Promise<ConflictResolutionResult> => {
|
||||
const [ownerAgentId, waitingAgentId] = conflict.conflictingAgentIds;
|
||||
|
||||
// Pause the waiting agent
|
||||
pauseAgentForConflict(waitingAgentId);
|
||||
|
||||
// Wait for owner to complete
|
||||
await waitForAgentCompletion(ownerAgentId);
|
||||
|
||||
// Resume waiting agent
|
||||
resumeAgent(waitingAgentId);
|
||||
|
||||
return {
|
||||
strategy: "serialize",
|
||||
winningAgentId: ownerAgentId,
|
||||
resolvedAt: Date.now(),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle abort-newer strategy - abort the agent that started later
|
||||
*/
|
||||
const handleAbortNewerStrategy = async (
|
||||
conflict: FileConflict,
|
||||
): Promise<ConflictResolutionResult> => {
|
||||
const state = multiAgentStore.getState();
|
||||
const agents = conflict.conflictingAgentIds
|
||||
.map((id) => state.instances.get(id))
|
||||
.filter((a): a is AgentInstance => a !== undefined);
|
||||
|
||||
// Sort by start time, newer agent is cancelled
|
||||
agents.sort((a, b) => a.startedAt - b.startedAt);
|
||||
const olderAgent = agents[0];
|
||||
const newerAgent = agents[1];
|
||||
|
||||
if (newerAgent) {
|
||||
multiAgentStore.updateInstanceStatus(
|
||||
newerAgent.id,
|
||||
"cancelled",
|
||||
MULTI_AGENT_ERRORS.CONFLICT_RESOLUTION_FAILED(conflict.filePath),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
strategy: "abort-newer",
|
||||
winningAgentId: olderAgent?.id,
|
||||
resolvedAt: Date.now(),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle merge strategy - placeholder for merge logic
|
||||
*/
|
||||
const handleMergeStrategy = async (
|
||||
_conflict: FileConflict,
|
||||
): Promise<ConflictResolutionResult> => {
|
||||
// Merge strategy requires comparing file contents and intelligently
|
||||
// combining changes. This is a placeholder - actual implementation
|
||||
// would need diff/patch logic.
|
||||
return {
|
||||
strategy: "merge-results",
|
||||
resolvedAt: Date.now(),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle isolated strategy - each agent works in isolation
|
||||
*/
|
||||
const handleIsolatedStrategy = async (
|
||||
_conflict: FileConflict,
|
||||
): Promise<ConflictResolutionResult> => {
|
||||
// In isolated mode, conflicts are expected and handled at merge time
|
||||
return {
|
||||
strategy: "isolated",
|
||||
resolvedAt: Date.now(),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Wait for an agent to complete
|
||||
*/
|
||||
const waitForAgentCompletion = (agentId: string): Promise<void> => {
|
||||
return new Promise((resolve) => {
|
||||
const checkInterval = setInterval(() => {
|
||||
const agent = multiAgentStore.getState().instances.get(agentId);
|
||||
if (!agent || ["completed", "error", "cancelled"].includes(agent.status)) {
|
||||
clearInterval(checkInterval);
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all current conflicts for an agent
|
||||
*/
|
||||
export const getConflictsForAgent = (agentId: string): FileConflict[] => {
|
||||
const conflicts = multiAgentStore.getUnresolvedConflicts();
|
||||
return conflicts.filter((c) => c.conflictingAgentIds.includes(agentId));
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear all file locks (for cleanup)
|
||||
*/
|
||||
export const clearAllLocks = (): void => {
|
||||
fileLocks.clear();
|
||||
pendingLocks.clear();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get lock statistics
|
||||
*/
|
||||
export const getLockStats = (): {
|
||||
lockedFiles: number;
|
||||
pendingRequests: number;
|
||||
} => {
|
||||
let pendingCount = 0;
|
||||
pendingLocks.forEach((pending) => {
|
||||
pendingCount += pending.length;
|
||||
});
|
||||
|
||||
return {
|
||||
lockedFiles: fileLocks.size,
|
||||
pendingRequests: pendingCount,
|
||||
};
|
||||
};
|
||||
403
src/services/multi-agent/executor.ts
Normal file
403
src/services/multi-agent/executor.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
/**
|
||||
* Multi-Agent Executor
|
||||
*
|
||||
* Orchestrates concurrent execution of multiple agents with
|
||||
* conflict detection, resource management, and result aggregation.
|
||||
*/
|
||||
|
||||
import type {
|
||||
MultiAgentRequest,
|
||||
MultiAgentResult,
|
||||
AgentSpawnConfig,
|
||||
AgentInstance,
|
||||
AgentExecutionResult,
|
||||
MultiAgentExecutorOptions,
|
||||
FileConflict,
|
||||
} from "@/types/multi-agent";
|
||||
import { multiAgentStore } from "@stores/core/multi-agent-store";
|
||||
import {
|
||||
MULTI_AGENT_DEFAULTS,
|
||||
MULTI_AGENT_ERRORS,
|
||||
MULTI_AGENT_LIMITS,
|
||||
} from "@/constants/multi-agent";
|
||||
import {
|
||||
createAgentInstance,
|
||||
startAgent,
|
||||
completeAgent,
|
||||
cancelAgent,
|
||||
validateSpawnConfig,
|
||||
} from "@/services/multi-agent/agent-manager";
|
||||
import {
|
||||
createToolContext,
|
||||
cleanupToolContext,
|
||||
} from "@/services/multi-agent/tool-context";
|
||||
import {
|
||||
resolveConflict,
|
||||
clearAllLocks,
|
||||
} from "@/services/multi-agent/conflict-handler";
|
||||
|
||||
/**
|
||||
* Execute multiple agents according to request configuration
|
||||
*/
|
||||
export const executeMultiAgent = async (
|
||||
request: Omit<MultiAgentRequest, "id">,
|
||||
options: MultiAgentExecutorOptions = {},
|
||||
): Promise<MultiAgentResult> => {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Validate request
|
||||
const validationError = validateRequest(request);
|
||||
if (validationError) {
|
||||
throw new Error(validationError);
|
||||
}
|
||||
|
||||
// Add request to store
|
||||
const requestId = multiAgentStore.addRequest(request);
|
||||
|
||||
// Track results
|
||||
const results: AgentInstance[] = [];
|
||||
const conflicts: FileConflict[] = [];
|
||||
|
||||
try {
|
||||
// Execute based on mode
|
||||
const executionHandlers: Record<
|
||||
typeof request.executionMode,
|
||||
() => Promise<void>
|
||||
> = {
|
||||
sequential: () => executeSequential(request.agents, options, results),
|
||||
parallel: () => executeParallel(request, options, results, conflicts),
|
||||
adaptive: () => executeAdaptive(request, options, results, conflicts),
|
||||
};
|
||||
|
||||
await executionHandlers[request.executionMode]();
|
||||
|
||||
// Aggregate results
|
||||
const result = aggregateResults(requestId, results, conflicts, startTime);
|
||||
|
||||
// Emit completion event
|
||||
options.onEvent?.({
|
||||
type: "execution_completed",
|
||||
result,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
return result;
|
||||
} finally {
|
||||
// Cleanup
|
||||
multiAgentStore.removeRequest(requestId);
|
||||
clearAllLocks();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate request configuration
|
||||
*/
|
||||
const validateRequest = (
|
||||
request: Omit<MultiAgentRequest, "id">,
|
||||
): string | null => {
|
||||
if (request.agents.length === 0) {
|
||||
return "At least one agent is required";
|
||||
}
|
||||
|
||||
if (request.agents.length > MULTI_AGENT_LIMITS.maxAgentsPerRequest) {
|
||||
return MULTI_AGENT_ERRORS.MAX_AGENTS_EXCEEDED(MULTI_AGENT_LIMITS.maxAgentsPerRequest);
|
||||
}
|
||||
|
||||
// Validate each agent config
|
||||
for (const config of request.agents) {
|
||||
const validation = validateSpawnConfig(config);
|
||||
if (!validation.valid) {
|
||||
return validation.errors[0];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute agents sequentially
|
||||
*/
|
||||
const executeSequential = async (
|
||||
configs: AgentSpawnConfig[],
|
||||
options: MultiAgentExecutorOptions,
|
||||
results: AgentInstance[],
|
||||
): Promise<void> => {
|
||||
for (const config of configs) {
|
||||
if (options.abortSignal?.aborted) {
|
||||
break;
|
||||
}
|
||||
|
||||
const instance = await executeSingleAgent(config, options);
|
||||
results.push(instance);
|
||||
|
||||
// Check for abort on error
|
||||
if (instance.status === "error" && options.abortSignal) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute agents in parallel
|
||||
*/
|
||||
const executeParallel = async (
|
||||
request: Omit<MultiAgentRequest, "id">,
|
||||
options: MultiAgentExecutorOptions,
|
||||
results: AgentInstance[],
|
||||
conflicts: FileConflict[],
|
||||
): Promise<void> => {
|
||||
const maxConcurrent = request.maxConcurrent ?? MULTI_AGENT_DEFAULTS.maxConcurrent;
|
||||
const chunks = chunkArray(request.agents, maxConcurrent);
|
||||
|
||||
for (const chunk of chunks) {
|
||||
if (options.abortSignal?.aborted) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Execute chunk in parallel
|
||||
const chunkPromises = chunk.map((config) =>
|
||||
executeSingleAgent(config, options),
|
||||
);
|
||||
|
||||
const chunkResults = await Promise.all(chunkPromises);
|
||||
results.push(...chunkResults);
|
||||
|
||||
// Collect any conflicts
|
||||
const newConflicts = multiAgentStore.getUnresolvedConflicts();
|
||||
conflicts.push(...newConflicts.filter((c) => !conflicts.includes(c)));
|
||||
|
||||
// Resolve conflicts if any
|
||||
for (const conflict of newConflicts) {
|
||||
await resolveConflict(conflict, request.conflictStrategy);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute agents adaptively (start parallel, serialize on conflict)
|
||||
*/
|
||||
const executeAdaptive = async (
|
||||
request: Omit<MultiAgentRequest, "id">,
|
||||
options: MultiAgentExecutorOptions,
|
||||
results: AgentInstance[],
|
||||
conflicts: FileConflict[],
|
||||
): Promise<void> => {
|
||||
const maxConcurrent = request.maxConcurrent ?? MULTI_AGENT_DEFAULTS.maxConcurrent;
|
||||
let conflictCount = 0;
|
||||
let useSequential = false;
|
||||
|
||||
// Start with parallel execution
|
||||
const remaining = [...request.agents];
|
||||
const running: Promise<AgentInstance>[] = [];
|
||||
|
||||
while (remaining.length > 0 || running.length > 0) {
|
||||
if (options.abortSignal?.aborted) {
|
||||
// Cancel all running agents
|
||||
multiAgentStore.getActiveInstances().forEach((instance) => {
|
||||
cancelAgent(instance.id, MULTI_AGENT_ERRORS.EXECUTION_ABORTED);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// Start new agents if under limit and not in sequential mode
|
||||
while (
|
||||
remaining.length > 0 &&
|
||||
running.length < maxConcurrent &&
|
||||
!useSequential
|
||||
) {
|
||||
const config = remaining.shift()!;
|
||||
running.push(executeSingleAgent(config, options));
|
||||
}
|
||||
|
||||
// If in sequential mode, wait for current to finish before starting next
|
||||
if (useSequential && remaining.length > 0 && running.length === 0) {
|
||||
const config = remaining.shift()!;
|
||||
running.push(executeSingleAgent(config, options));
|
||||
}
|
||||
|
||||
// Wait for at least one to complete
|
||||
if (running.length > 0) {
|
||||
const completed = await Promise.race(
|
||||
running.map((p, i) => p.then((result) => ({ result, index: i }))),
|
||||
);
|
||||
|
||||
results.push(completed.result);
|
||||
running.splice(completed.index, 1);
|
||||
|
||||
// Check for new conflicts
|
||||
const newConflicts = multiAgentStore.getUnresolvedConflicts();
|
||||
for (const conflict of newConflicts) {
|
||||
if (!conflicts.includes(conflict)) {
|
||||
conflicts.push(conflict);
|
||||
conflictCount++;
|
||||
|
||||
// Resolve conflict
|
||||
await resolveConflict(conflict, request.conflictStrategy);
|
||||
|
||||
// Switch to sequential mode if too many conflicts
|
||||
if (conflictCount >= MULTI_AGENT_LIMITS.maxConflictsBeforeAbort) {
|
||||
useSequential = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute a single agent
|
||||
*/
|
||||
const executeSingleAgent = async (
|
||||
config: AgentSpawnConfig,
|
||||
options: MultiAgentExecutorOptions,
|
||||
): Promise<AgentInstance> => {
|
||||
// Create instance
|
||||
const instanceOrError = createAgentInstance(config);
|
||||
if ("error" in instanceOrError) {
|
||||
throw new Error(instanceOrError.error);
|
||||
}
|
||||
|
||||
const instance = instanceOrError;
|
||||
|
||||
// Create tool context
|
||||
createToolContext(
|
||||
instance.id,
|
||||
process.cwd(),
|
||||
config.contextFiles,
|
||||
[],
|
||||
);
|
||||
|
||||
// Start agent
|
||||
startAgent(instance.id);
|
||||
|
||||
try {
|
||||
// Execute agent task
|
||||
const result = await executeAgentTask(instance, config, options);
|
||||
|
||||
// Complete agent
|
||||
completeAgent(instance.id, result);
|
||||
|
||||
return multiAgentStore.getState().instances.get(instance.id) ?? instance;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
completeAgent(instance.id, {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
filesModified: [],
|
||||
toolCallCount: 0,
|
||||
duration: Date.now() - instance.startedAt,
|
||||
});
|
||||
|
||||
return multiAgentStore.getState().instances.get(instance.id) ?? instance;
|
||||
} finally {
|
||||
// Cleanup context
|
||||
cleanupToolContext(instance.id);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute the actual agent task
|
||||
* This is a placeholder - actual implementation would integrate with
|
||||
* the chat/provider system
|
||||
*/
|
||||
const executeAgentTask = async (
|
||||
instance: AgentInstance,
|
||||
config: AgentSpawnConfig,
|
||||
_options: MultiAgentExecutorOptions,
|
||||
): Promise<AgentExecutionResult> => {
|
||||
const startTime = Date.now();
|
||||
|
||||
// This is where the actual agent execution would happen
|
||||
// For now, return a placeholder result
|
||||
// In real implementation, this would:
|
||||
// 1. Build system prompt from agent definition
|
||||
// 2. Send task to LLM provider
|
||||
// 3. Handle tool calls
|
||||
// 4. Track file modifications
|
||||
// 5. Return result
|
||||
|
||||
// Placeholder implementation
|
||||
return {
|
||||
success: true,
|
||||
output: `Agent ${instance.definition.name} completed task: ${config.task}`,
|
||||
filesModified: [],
|
||||
toolCallCount: 0,
|
||||
duration: Date.now() - startTime,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Aggregate results from all agents
|
||||
*/
|
||||
const aggregateResults = (
|
||||
requestId: string,
|
||||
agents: AgentInstance[],
|
||||
conflicts: FileConflict[],
|
||||
startTime: number,
|
||||
): MultiAgentResult => {
|
||||
const successful = agents.filter((a) => a.status === "completed").length;
|
||||
const failed = agents.filter((a) => a.status === "error").length;
|
||||
const cancelled = agents.filter((a) => a.status === "cancelled").length;
|
||||
|
||||
// Aggregate output from all successful agents
|
||||
const outputs = agents
|
||||
.filter((a) => a.status === "completed" && a.result?.output)
|
||||
.map((a) => a.result!.output);
|
||||
|
||||
return {
|
||||
requestId,
|
||||
agents,
|
||||
successful,
|
||||
failed,
|
||||
cancelled,
|
||||
conflicts,
|
||||
totalDuration: Date.now() - startTime,
|
||||
aggregatedOutput: outputs.length > 0 ? outputs.join("\n\n---\n\n") : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Split array into chunks
|
||||
*/
|
||||
const chunkArray = <T>(array: T[], size: number): T[][] => {
|
||||
const chunks: T[][] = [];
|
||||
for (let i = 0; i < array.length; i += size) {
|
||||
chunks.push(array.slice(i, i + size));
|
||||
}
|
||||
return chunks;
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel a running multi-agent execution
|
||||
*/
|
||||
export const cancelExecution = (requestId: string): void => {
|
||||
const request = multiAgentStore.getState().activeRequests.get(requestId);
|
||||
if (!request) return;
|
||||
|
||||
// Cancel all agents associated with this request
|
||||
multiAgentStore.getActiveInstances().forEach((instance) => {
|
||||
cancelAgent(instance.id, MULTI_AGENT_ERRORS.EXECUTION_ABORTED);
|
||||
});
|
||||
|
||||
multiAgentStore.removeRequest(requestId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get execution status
|
||||
*/
|
||||
export const getExecutionStatus = (): {
|
||||
isExecuting: boolean;
|
||||
activeRequests: number;
|
||||
runningAgents: number;
|
||||
conflicts: number;
|
||||
} => {
|
||||
const state = multiAgentStore.getState();
|
||||
|
||||
return {
|
||||
isExecuting: state.isExecuting,
|
||||
activeRequests: state.activeRequests.size,
|
||||
runningAgents: multiAgentStore.getActiveInstances().length,
|
||||
conflicts: multiAgentStore.getUnresolvedConflicts().length,
|
||||
};
|
||||
};
|
||||
239
src/services/multi-agent/tool-context.ts
Normal file
239
src/services/multi-agent/tool-context.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* Agent Tool Context
|
||||
*
|
||||
* Provides isolated tool execution context for each agent,
|
||||
* tracking file modifications and enforcing permissions.
|
||||
*/
|
||||
|
||||
import type { AgentToolContext } from "@/types/multi-agent";
|
||||
import { multiAgentStore } from "@stores/core/multi-agent-store";
|
||||
import {
|
||||
acquireFileLock,
|
||||
releaseFileLock,
|
||||
releaseAllLocks,
|
||||
detectConflict,
|
||||
} from "@/services/multi-agent/conflict-handler";
|
||||
|
||||
/**
|
||||
* Active tool contexts by agent ID
|
||||
*/
|
||||
const activeContexts: Map<string, AgentToolContext> = new Map();
|
||||
|
||||
/**
|
||||
* Create a tool context for an agent
|
||||
*/
|
||||
export const createToolContext = (
|
||||
agentId: string,
|
||||
workingDir: string,
|
||||
allowedPaths: string[] = [],
|
||||
deniedPaths: string[] = [],
|
||||
): AgentToolContext => {
|
||||
const context: AgentToolContext = {
|
||||
agentId,
|
||||
workingDir,
|
||||
allowedPaths,
|
||||
deniedPaths,
|
||||
modifiedFiles: new Set(),
|
||||
lockedFiles: new Set(),
|
||||
};
|
||||
|
||||
activeContexts.set(agentId, context);
|
||||
return context;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get tool context for an agent
|
||||
*/
|
||||
export const getToolContext = (agentId: string): AgentToolContext | null => {
|
||||
return activeContexts.get(agentId) ?? null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a path is allowed for an agent
|
||||
*/
|
||||
export const isPathAllowed = (
|
||||
agentId: string,
|
||||
filePath: string,
|
||||
): boolean => {
|
||||
const context = activeContexts.get(agentId);
|
||||
if (!context) return false;
|
||||
|
||||
// Check denied paths first (higher priority)
|
||||
for (const denied of context.deniedPaths) {
|
||||
if (filePath.startsWith(denied)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If no allowed paths specified, allow all (except denied)
|
||||
if (context.allowedPaths.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if path matches any allowed path
|
||||
for (const allowed of context.allowedPaths) {
|
||||
if (filePath.startsWith(allowed)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Request write access to a file
|
||||
* Returns true if access granted, false if conflict or denied
|
||||
*/
|
||||
export const requestWriteAccess = async (
|
||||
agentId: string,
|
||||
filePath: string,
|
||||
): Promise<{ granted: boolean; conflict?: boolean; reason?: string }> => {
|
||||
const context = activeContexts.get(agentId);
|
||||
if (!context) {
|
||||
return { granted: false, reason: "No active context for agent" };
|
||||
}
|
||||
|
||||
// Check path permissions
|
||||
if (!isPathAllowed(agentId, filePath)) {
|
||||
return { granted: false, reason: "Path not allowed for this agent" };
|
||||
}
|
||||
|
||||
// Detect conflicts with other agents
|
||||
const conflict = detectConflict(agentId, filePath);
|
||||
if (conflict) {
|
||||
return { granted: false, conflict: true, reason: "File locked by another agent" };
|
||||
}
|
||||
|
||||
// Acquire file lock
|
||||
const acquired = await acquireFileLock(agentId, filePath);
|
||||
if (!acquired) {
|
||||
return { granted: false, conflict: true, reason: "Could not acquire file lock" };
|
||||
}
|
||||
|
||||
// Track locked file
|
||||
context.lockedFiles.add(filePath);
|
||||
return { granted: true };
|
||||
};
|
||||
|
||||
/**
|
||||
* Record a file modification
|
||||
*/
|
||||
export const recordModification = (
|
||||
agentId: string,
|
||||
filePath: string,
|
||||
): void => {
|
||||
const context = activeContexts.get(agentId);
|
||||
if (!context) return;
|
||||
|
||||
context.modifiedFiles.add(filePath);
|
||||
multiAgentStore.addModifiedFile(agentId, filePath);
|
||||
};
|
||||
|
||||
/**
|
||||
* Release write access to a file
|
||||
*/
|
||||
export const releaseWriteAccess = (
|
||||
agentId: string,
|
||||
filePath: string,
|
||||
): void => {
|
||||
const context = activeContexts.get(agentId);
|
||||
if (!context) return;
|
||||
|
||||
context.lockedFiles.delete(filePath);
|
||||
releaseFileLock(agentId, filePath);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all files modified by an agent
|
||||
*/
|
||||
export const getModifiedFiles = (agentId: string): string[] => {
|
||||
const context = activeContexts.get(agentId);
|
||||
if (!context) return [];
|
||||
|
||||
return Array.from(context.modifiedFiles);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all files currently locked by an agent
|
||||
*/
|
||||
export const getLockedFiles = (agentId: string): string[] => {
|
||||
const context = activeContexts.get(agentId);
|
||||
if (!context) return [];
|
||||
|
||||
return Array.from(context.lockedFiles);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clean up tool context for an agent
|
||||
*/
|
||||
export const cleanupToolContext = (agentId: string): void => {
|
||||
const context = activeContexts.get(agentId);
|
||||
if (!context) return;
|
||||
|
||||
// Release all locks
|
||||
releaseAllLocks(agentId);
|
||||
|
||||
// Remove context
|
||||
activeContexts.delete(agentId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clean up all tool contexts
|
||||
*/
|
||||
export const cleanupAllContexts = (): void => {
|
||||
activeContexts.forEach((_, agentId) => {
|
||||
cleanupToolContext(agentId);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get context statistics
|
||||
*/
|
||||
export const getContextStats = (): {
|
||||
activeContexts: number;
|
||||
totalModifiedFiles: number;
|
||||
totalLockedFiles: number;
|
||||
} => {
|
||||
let modifiedCount = 0;
|
||||
let lockedCount = 0;
|
||||
|
||||
activeContexts.forEach((context) => {
|
||||
modifiedCount += context.modifiedFiles.size;
|
||||
lockedCount += context.lockedFiles.size;
|
||||
});
|
||||
|
||||
return {
|
||||
activeContexts: activeContexts.size,
|
||||
totalModifiedFiles: modifiedCount,
|
||||
totalLockedFiles: lockedCount,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a wrapped tool executor that uses the agent context
|
||||
*/
|
||||
export const createContextualToolExecutor = <TArgs, TResult>(
|
||||
agentId: string,
|
||||
executor: (args: TArgs) => Promise<TResult>,
|
||||
options: {
|
||||
requiresWriteAccess?: (args: TArgs) => string | null;
|
||||
} = {},
|
||||
): ((args: TArgs) => Promise<TResult>) => {
|
||||
return async (args: TArgs): Promise<TResult> => {
|
||||
// Check if write access is required
|
||||
if (options.requiresWriteAccess) {
|
||||
const filePath = options.requiresWriteAccess(args);
|
||||
if (filePath) {
|
||||
const access = await requestWriteAccess(agentId, filePath);
|
||||
if (!access.granted) {
|
||||
throw new Error(access.reason ?? "Write access denied");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the tool
|
||||
const result = await executor(args);
|
||||
|
||||
return result;
|
||||
};
|
||||
};
|
||||
@@ -4,7 +4,7 @@
|
||||
* Provides functions for agents to create and update plans
|
||||
*/
|
||||
|
||||
import { todoStore } from "@stores/todo-store";
|
||||
import { todoStore } from "@stores/core/todo-store";
|
||||
import type { TodoStatus } from "@/types/todo";
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
PluginCommandDefinition,
|
||||
PluginLoadResult,
|
||||
} from "@/types/plugin";
|
||||
import type { FunctionDefinition, ToolDefinition } from "@tools/types";
|
||||
import type { FunctionDefinition, ToolDefinition } from "@tools/core/types";
|
||||
import type { HookDefinition } from "@/types/hooks";
|
||||
import {
|
||||
discoverPlugins,
|
||||
|
||||
@@ -24,9 +24,9 @@ import type {
|
||||
import { chat as providerChat } from "@providers/index";
|
||||
import { getTool, getToolsForApi, refreshMCPTools } from "@tools/index";
|
||||
import type { ToolContext, ToolCall, ToolResult } from "@/types/tools";
|
||||
import { initializePermissions } from "@services/permissions";
|
||||
import { initializePermissions } from "@services/core/permissions";
|
||||
import { MAX_ITERATIONS } from "@constants/agent";
|
||||
import { usageStore } from "@stores/usage-store";
|
||||
import { usageStore } from "@stores/core/usage-store";
|
||||
import type {
|
||||
TaskConstraints,
|
||||
CompressibleMessage,
|
||||
|
||||
9
src/stores/core/index.ts
Normal file
9
src/stores/core/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Store Core - Re-exports all stores
|
||||
*/
|
||||
|
||||
export * from "./vim-store";
|
||||
export * from "./theme-store";
|
||||
export * from "./usage-store";
|
||||
export * from "./todo-store";
|
||||
export * from "./multi-agent-store";
|
||||
376
src/stores/core/multi-agent-store.ts
Normal file
376
src/stores/core/multi-agent-store.ts
Normal file
@@ -0,0 +1,376 @@
|
||||
/**
|
||||
* Multi-Agent Store
|
||||
*
|
||||
* Zustand store for managing multi-agent execution state.
|
||||
*/
|
||||
|
||||
import { createStore } from "zustand/vanilla";
|
||||
import type {
|
||||
AgentInstance,
|
||||
AgentInstanceStatus,
|
||||
AgentToolCall,
|
||||
FileConflict,
|
||||
MultiAgentRequest,
|
||||
MultiAgentEvent,
|
||||
ConflictResolutionResult,
|
||||
} from "@/types/multi-agent";
|
||||
import type { ChatMessage } from "@/types/common";
|
||||
import {
|
||||
AGENT_ID_PREFIX,
|
||||
REQUEST_ID_PREFIX,
|
||||
} from "@/constants/multi-agent";
|
||||
|
||||
/**
|
||||
* Multi-agent store state
|
||||
*/
|
||||
interface MultiAgentState {
|
||||
activeRequests: Map<string, MultiAgentRequest>;
|
||||
instances: Map<string, AgentInstance>;
|
||||
conflicts: FileConflict[];
|
||||
isExecuting: boolean;
|
||||
lastError: string | null;
|
||||
events: MultiAgentEvent[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial state
|
||||
*/
|
||||
const initialState: MultiAgentState = {
|
||||
activeRequests: new Map(),
|
||||
instances: new Map(),
|
||||
conflicts: [],
|
||||
isExecuting: false,
|
||||
lastError: null,
|
||||
events: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Vanilla Zustand store
|
||||
*/
|
||||
const store = createStore<MultiAgentState>(() => ({
|
||||
...initialState,
|
||||
}));
|
||||
|
||||
/**
|
||||
* Generate unique agent ID
|
||||
*/
|
||||
const generateAgentId = (): string => {
|
||||
return `${AGENT_ID_PREFIX}${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate unique request ID
|
||||
*/
|
||||
const generateRequestId = (): string => {
|
||||
return `${REQUEST_ID_PREFIX}${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a new request
|
||||
*/
|
||||
const addRequest = (request: Omit<MultiAgentRequest, "id">): string => {
|
||||
const id = generateRequestId();
|
||||
const fullRequest: MultiAgentRequest = { ...request, id };
|
||||
|
||||
store.setState((state) => {
|
||||
const newRequests = new Map(state.activeRequests);
|
||||
newRequests.set(id, fullRequest);
|
||||
return {
|
||||
activeRequests: newRequests,
|
||||
isExecuting: true,
|
||||
};
|
||||
});
|
||||
|
||||
return id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a request
|
||||
*/
|
||||
const removeRequest = (requestId: string): void => {
|
||||
store.setState((state) => {
|
||||
const newRequests = new Map(state.activeRequests);
|
||||
newRequests.delete(requestId);
|
||||
return {
|
||||
activeRequests: newRequests,
|
||||
isExecuting: newRequests.size > 0,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a new agent instance
|
||||
*/
|
||||
const addInstance = (instance: Omit<AgentInstance, "id">): string => {
|
||||
const id = generateAgentId();
|
||||
const fullInstance: AgentInstance = { ...instance, id };
|
||||
|
||||
store.setState((state) => {
|
||||
const newInstances = new Map(state.instances);
|
||||
newInstances.set(id, fullInstance);
|
||||
return { instances: newInstances };
|
||||
});
|
||||
|
||||
addEvent({ type: "agent_started", agentId: id, timestamp: Date.now() });
|
||||
return id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update agent instance status
|
||||
*/
|
||||
const updateInstanceStatus = (
|
||||
agentId: string,
|
||||
status: AgentInstanceStatus,
|
||||
error?: string,
|
||||
): void => {
|
||||
store.setState((state) => {
|
||||
const instance = state.instances.get(agentId);
|
||||
if (!instance) return state;
|
||||
|
||||
const newInstances = new Map(state.instances);
|
||||
newInstances.set(agentId, {
|
||||
...instance,
|
||||
status,
|
||||
error,
|
||||
completedAt: ["completed", "error", "cancelled"].includes(status)
|
||||
? Date.now()
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return { instances: newInstances };
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a message to agent conversation
|
||||
*/
|
||||
const addAgentMessage = (agentId: string, message: ChatMessage): void => {
|
||||
store.setState((state) => {
|
||||
const instance = state.instances.get(agentId);
|
||||
if (!instance) return state;
|
||||
|
||||
const newInstances = new Map(state.instances);
|
||||
newInstances.set(agentId, {
|
||||
...instance,
|
||||
conversation: {
|
||||
...instance.conversation,
|
||||
messages: [...instance.conversation.messages, message],
|
||||
},
|
||||
});
|
||||
|
||||
return { instances: newInstances };
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a tool call to agent conversation
|
||||
*/
|
||||
const addToolCall = (agentId: string, toolCall: AgentToolCall): void => {
|
||||
store.setState((state) => {
|
||||
const instance = state.instances.get(agentId);
|
||||
if (!instance) return state;
|
||||
|
||||
const newInstances = new Map(state.instances);
|
||||
newInstances.set(agentId, {
|
||||
...instance,
|
||||
conversation: {
|
||||
...instance.conversation,
|
||||
toolCalls: [...instance.conversation.toolCalls, toolCall],
|
||||
},
|
||||
});
|
||||
|
||||
return { instances: newInstances };
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update tool call result
|
||||
*/
|
||||
const updateToolCallResult = (
|
||||
agentId: string,
|
||||
toolCallId: string,
|
||||
result: AgentToolCall["result"],
|
||||
): void => {
|
||||
store.setState((state) => {
|
||||
const instance = state.instances.get(agentId);
|
||||
if (!instance) return state;
|
||||
|
||||
const newInstances = new Map(state.instances);
|
||||
newInstances.set(agentId, {
|
||||
...instance,
|
||||
conversation: {
|
||||
...instance.conversation,
|
||||
toolCalls: instance.conversation.toolCalls.map((tc) =>
|
||||
tc.id === toolCallId ? { ...tc, result } : tc,
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
return { instances: newInstances };
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a file to agent's modified files list
|
||||
*/
|
||||
const addModifiedFile = (agentId: string, filePath: string): void => {
|
||||
store.setState((state) => {
|
||||
const instance = state.instances.get(agentId);
|
||||
if (!instance) return state;
|
||||
|
||||
if (instance.modifiedFiles.includes(filePath)) return state;
|
||||
|
||||
const newInstances = new Map(state.instances);
|
||||
newInstances.set(agentId, {
|
||||
...instance,
|
||||
modifiedFiles: [...instance.modifiedFiles, filePath],
|
||||
});
|
||||
|
||||
return { instances: newInstances };
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a conflict
|
||||
*/
|
||||
const addConflict = (conflict: FileConflict): void => {
|
||||
store.setState((state) => ({
|
||||
conflicts: [...state.conflicts, conflict],
|
||||
}));
|
||||
|
||||
addEvent({ type: "conflict_detected", conflict, timestamp: Date.now() });
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve a conflict
|
||||
*/
|
||||
const resolveConflict = (
|
||||
filePath: string,
|
||||
resolution: ConflictResolutionResult,
|
||||
): void => {
|
||||
store.setState((state) => {
|
||||
const updatedConflicts = state.conflicts.map((c) =>
|
||||
c.filePath === filePath ? { ...c, resolution } : c,
|
||||
);
|
||||
return { conflicts: updatedConflicts };
|
||||
});
|
||||
|
||||
const conflict = store.getState().conflicts.find((c) => c.filePath === filePath);
|
||||
if (conflict) {
|
||||
addEvent({
|
||||
type: "conflict_resolved",
|
||||
conflict: { ...conflict, resolution },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add an event
|
||||
*/
|
||||
const addEvent = (event: MultiAgentEvent): void => {
|
||||
store.setState((state) => ({
|
||||
events: [...state.events.slice(-99), event], // Keep last 100 events
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Set last error
|
||||
*/
|
||||
const setError = (error: string | null): void => {
|
||||
store.setState({ lastError: error });
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear all state
|
||||
*/
|
||||
const clear = (): void => {
|
||||
store.setState({ ...initialState });
|
||||
};
|
||||
|
||||
/**
|
||||
* Get active agent instances
|
||||
*/
|
||||
const getActiveInstances = (): AgentInstance[] => {
|
||||
const { instances } = store.getState();
|
||||
return Array.from(instances.values()).filter(
|
||||
(i) => i.status === "running" || i.status === "waiting_conflict",
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get instances by status
|
||||
*/
|
||||
const getInstancesByStatus = (status: AgentInstanceStatus): AgentInstance[] => {
|
||||
const { instances } = store.getState();
|
||||
return Array.from(instances.values()).filter((i) => i.status === status);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get unresolved conflicts
|
||||
*/
|
||||
const getUnresolvedConflicts = (): FileConflict[] => {
|
||||
const { conflicts } = store.getState();
|
||||
return conflicts.filter((c) => !c.resolution);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a file is being modified by any agent
|
||||
*/
|
||||
const isFileBeingModified = (filePath: string): boolean => {
|
||||
const activeInstances = getActiveInstances();
|
||||
return activeInstances.some((i) => i.modifiedFiles.includes(filePath));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get agent modifying a file
|
||||
*/
|
||||
const getAgentModifyingFile = (filePath: string): AgentInstance | null => {
|
||||
const activeInstances = getActiveInstances();
|
||||
return activeInstances.find((i) => i.modifiedFiles.includes(filePath)) ?? null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Multi-agent store API
|
||||
*/
|
||||
export const multiAgentStore = {
|
||||
// Request management
|
||||
addRequest,
|
||||
removeRequest,
|
||||
|
||||
// Instance management
|
||||
addInstance,
|
||||
updateInstanceStatus,
|
||||
addAgentMessage,
|
||||
addToolCall,
|
||||
updateToolCallResult,
|
||||
addModifiedFile,
|
||||
|
||||
// Conflict management
|
||||
addConflict,
|
||||
resolveConflict,
|
||||
|
||||
// Events
|
||||
addEvent,
|
||||
|
||||
// Error handling
|
||||
setError,
|
||||
|
||||
// State management
|
||||
clear,
|
||||
|
||||
// Queries
|
||||
getActiveInstances,
|
||||
getInstancesByStatus,
|
||||
getUnresolvedConflicts,
|
||||
isFileBeingModified,
|
||||
getAgentModifyingFile,
|
||||
|
||||
// State access
|
||||
getState: store.getState,
|
||||
subscribe: store.subscribe,
|
||||
};
|
||||
|
||||
// Export store for React hooks
|
||||
export { store as multiAgentStoreVanilla };
|
||||
export type { MultiAgentState };
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user