241 lines
8.6 KiB
TypeScript
241 lines
8.6 KiB
TypeScript
/**
|
|
* SubagentLocator - Locates and manages subagent files.
|
|
*
|
|
* Responsibilities:
|
|
* - Check if sessions have subagent files
|
|
* - List subagent files for a session
|
|
* - Handle both NEW and OLD subagent directory structures:
|
|
* - NEW: {projectId}/{sessionId}/subagents/agent-{agentId}.jsonl
|
|
* - OLD: {projectId}/agent-{agentId}.jsonl (legacy, still supported)
|
|
* - Determine subagent ownership for OLD structure
|
|
*/
|
|
|
|
import { LocalFileSystemProvider } from '@main/services/infrastructure/LocalFileSystemProvider';
|
|
import { buildSubagentsPath, extractBaseDir } from '@main/utils/pathDecoder';
|
|
import { createLogger } from '@shared/utils/logger';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
import type { FileSystemProvider } from '@main/services/infrastructure/FileSystemProvider';
|
|
|
|
const logger = createLogger('Discovery:SubagentLocator');
|
|
|
|
/**
|
|
* SubagentLocator provides methods for locating subagent files.
|
|
*/
|
|
export class SubagentLocator {
|
|
private readonly projectsDir: string;
|
|
private readonly fsProvider: FileSystemProvider;
|
|
|
|
constructor(projectsDir: string, fsProvider?: FileSystemProvider) {
|
|
this.projectsDir = projectsDir;
|
|
this.fsProvider = fsProvider ?? new LocalFileSystemProvider();
|
|
}
|
|
|
|
/**
|
|
* Checks if a session has subagent files (async).
|
|
* Uses the FileSystemProvider for filesystem access.
|
|
*
|
|
* @param projectId - The project ID
|
|
* @param sessionId - The session ID
|
|
* @returns Promise resolving to true if subagents exist
|
|
*/
|
|
async hasSubagents(projectId: string, sessionId: string): Promise<boolean> {
|
|
// Check NEW structure: {projectId}/{sessionId}/subagents/
|
|
const newSubagentsPath = this.getSubagentsPath(projectId, sessionId);
|
|
if (await this.fsProvider.exists(newSubagentsPath)) {
|
|
try {
|
|
const entries = await this.fsProvider.readdir(newSubagentsPath);
|
|
const subagentFiles = entries.filter(
|
|
(entry) => entry.name.startsWith('agent-') && entry.name.endsWith('.jsonl')
|
|
);
|
|
|
|
// Check if at least one subagent file has content (not empty)
|
|
for (const entry of subagentFiles) {
|
|
const filePath = path.join(newSubagentsPath, entry.name);
|
|
try {
|
|
const stats = await this.fsProvider.stat(filePath);
|
|
// File must have size > 0 and contain at least one line
|
|
if (stats.size > 0) {
|
|
const content = await this.fsProvider.readFile(filePath);
|
|
if (content.trim().length > 0) {
|
|
return true;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// Skip this file if we can't read it - log for debugging
|
|
logger.debug(`SubagentLocator: Could not read file ${filePath}:`, error);
|
|
continue;
|
|
}
|
|
}
|
|
} catch {
|
|
// Ignore errors
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Checks if a session has subagent files (session-specific only).
|
|
* Only checks the NEW structure: {projectId}/{sessionId}/subagents/
|
|
* Verifies that at least one subagent file has non-empty content.
|
|
*
|
|
* @param projectId - The project ID
|
|
* @param sessionId - The session ID
|
|
* @returns true if subagents exist
|
|
*/
|
|
hasSubagentsSync(projectId: string, sessionId: string): boolean {
|
|
// Check NEW structure: {projectId}/{sessionId}/subagents/
|
|
const newSubagentsPath = this.getSubagentsPath(projectId, sessionId);
|
|
if (fs.existsSync(newSubagentsPath)) {
|
|
try {
|
|
const entries = fs.readdirSync(newSubagentsPath);
|
|
const subagentFiles = entries.filter(
|
|
(name) => name.startsWith('agent-') && name.endsWith('.jsonl')
|
|
);
|
|
|
|
// Check if at least one subagent file has content (not empty)
|
|
for (const fileName of subagentFiles) {
|
|
const filePath = path.join(newSubagentsPath, fileName);
|
|
try {
|
|
const stats = fs.statSync(filePath);
|
|
// File must have size > 0 and contain at least one line
|
|
if (stats.size > 0) {
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
if (content.trim().length > 0) {
|
|
return true;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// Skip this file if we can't read it - log for debugging
|
|
logger.debug(`SubagentLocator: Could not read file ${filePath}:`, error);
|
|
continue;
|
|
}
|
|
}
|
|
} catch {
|
|
// Ignore errors
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Lists all subagent files for a session from both NEW and OLD structures.
|
|
* Returns NEW structure files first, then OLD structure files.
|
|
*
|
|
* @param projectId - The project ID
|
|
* @param sessionId - The session ID
|
|
* @returns Promise resolving to array of file paths
|
|
*/
|
|
async listSubagentFiles(projectId: string, sessionId: string): Promise<string[]> {
|
|
const allFiles: string[] = [];
|
|
|
|
try {
|
|
// Scan NEW structure: {projectId}/{sessionId}/subagents/agent-*.jsonl
|
|
const newSubagentsPath = this.getSubagentsPath(projectId, sessionId);
|
|
if (await this.fsProvider.exists(newSubagentsPath)) {
|
|
const entries = await this.fsProvider.readdir(newSubagentsPath);
|
|
const newFiles = entries
|
|
.filter(
|
|
(entry) =>
|
|
entry.isFile() && entry.name.startsWith('agent-') && entry.name.endsWith('.jsonl')
|
|
)
|
|
.map((entry) => path.join(newSubagentsPath, entry.name));
|
|
allFiles.push(...newFiles);
|
|
}
|
|
} catch (error) {
|
|
logger.error(`Error scanning NEW subagent structure for session ${sessionId}:`, error);
|
|
}
|
|
|
|
try {
|
|
// Scan OLD structure: {projectId}/agent-*.jsonl
|
|
// Must filter by sessionId since all sessions share the same project root
|
|
const oldFiles = await this.getProjectRootSubagentFiles(projectId, sessionId);
|
|
allFiles.push(...oldFiles);
|
|
} catch (error) {
|
|
logger.error(`Error scanning OLD subagent structure for project ${projectId}:`, error);
|
|
}
|
|
|
|
return allFiles;
|
|
}
|
|
|
|
/**
|
|
* Gets subagent files from project root (OLD structure).
|
|
* Scans {projectId}/agent-*.jsonl files and filters by sessionId.
|
|
*
|
|
* In the OLD structure, all subagent files are in the project root,
|
|
* so we must read each file's first line to check if it belongs to the session.
|
|
*
|
|
* @param projectId - The project ID
|
|
* @param sessionId - The session ID
|
|
* @returns Promise resolving to array of file paths
|
|
*/
|
|
async getProjectRootSubagentFiles(projectId: string, sessionId: string): Promise<string[]> {
|
|
try {
|
|
const projectPath = path.join(this.projectsDir, extractBaseDir(projectId));
|
|
|
|
if (!(await this.fsProvider.exists(projectPath))) {
|
|
return [];
|
|
}
|
|
|
|
const entries = await this.fsProvider.readdir(projectPath);
|
|
const agentFiles = entries
|
|
.filter((entry) => entry.name.startsWith('agent-') && entry.name.endsWith('.jsonl'))
|
|
.map((entry) => path.join(projectPath, entry.name));
|
|
|
|
// Filter files by checking if their sessionId matches
|
|
const matchingFiles: string[] = [];
|
|
for (const filePath of agentFiles) {
|
|
if (await this.subagentBelongsToSession(filePath, sessionId)) {
|
|
matchingFiles.push(filePath);
|
|
}
|
|
}
|
|
|
|
return matchingFiles;
|
|
} catch (error) {
|
|
logger.error(`Error reading project root for subagent files:`, error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if a subagent file belongs to a specific session by reading its first line.
|
|
* Subagent files have a sessionId field that points to the parent session.
|
|
*
|
|
* @param filePath - Path to the subagent file
|
|
* @param sessionId - The session ID to check
|
|
* @returns Promise resolving to true if the subagent belongs to the session
|
|
*/
|
|
async subagentBelongsToSession(filePath: string, sessionId: string): Promise<boolean> {
|
|
try {
|
|
// Read just the first line to check sessionId
|
|
const content = await this.fsProvider.readFile(filePath);
|
|
const firstNewline = content.indexOf('\n');
|
|
const firstLine = firstNewline > 0 ? content.slice(0, firstNewline) : content;
|
|
|
|
if (!firstLine.trim()) {
|
|
return false;
|
|
}
|
|
|
|
const entry = JSON.parse(firstLine) as { sessionId?: string };
|
|
return entry.sessionId === sessionId;
|
|
} catch (error) {
|
|
// If we can't read or parse the file, don't include it - log for debugging
|
|
logger.debug(`SubagentLocator: Could not parse file ${filePath}:`, error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the path to the subagents directory.
|
|
*
|
|
* @param projectId - The project ID
|
|
* @param sessionId - The session ID
|
|
* @returns Path to the subagents directory
|
|
*/
|
|
getSubagentsPath(projectId: string, sessionId: string): string {
|
|
return buildSubagentsPath(this.projectsDir, projectId, sessionId);
|
|
}
|
|
}
|