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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user