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:
2026-02-04 18:47:03 -05:00
parent c1b4384890
commit f0609e423e
191 changed files with 3162 additions and 824 deletions

View File

@@ -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";

View File

@@ -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";

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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

View File

@@ -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);

View 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";

View File

@@ -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

View 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,
};
};

View 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,
};
};

View 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,
};
};

View 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;
};
};

View File

@@ -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";
/**

View File

@@ -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,

View File

@@ -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,