diff --git a/packages/agent-core-v2/src/agent/media/registerMediaTools.ts b/packages/agent-core-v2/src/agent/media/registerMediaTools.ts index 9131a3055..6872575ca 100644 --- a/packages/agent-core-v2/src/agent/media/registerMediaTools.ts +++ b/packages/agent-core-v2/src/agent/media/registerMediaTools.ts @@ -18,13 +18,13 @@ import type { Model } from '#/app/model'; import { toDisposable, type IDisposable } from '#/_base/di'; import type { WorkspaceConfig } from '#/_base/tools/support/workspace'; -import type { ISessionAgentFileSystem } from '#/session/agentFs'; +import type { IHostFileSystem } from '#/os/interface/hostFileSystem'; import type { IHostEnvironment } from '#/os/interface/hostEnvironment'; import type { IAgentToolRegistryService } from '#/agent/toolRegistry'; import { ReadMediaFileTool, type VideoUploader } from '#/agent/media/tools/read-media'; export interface RegisterMediaToolsDeps { - readonly fs: ISessionAgentFileSystem; + readonly fs: IHostFileSystem; readonly env: IHostEnvironment; readonly workspace: WorkspaceConfig; readonly capabilities: ModelCapability; diff --git a/packages/agent-core-v2/src/agent/media/tools/read-media.ts b/packages/agent-core-v2/src/agent/media/tools/read-media.ts index 45bd89b63..2e7d18e0e 100644 --- a/packages/agent-core-v2/src/agent/media/tools/read-media.ts +++ b/packages/agent-core-v2/src/agent/media/tools/read-media.ts @@ -27,7 +27,7 @@ import type { } from '#/app/llmProtocol'; import { z } from 'zod'; -import { ISessionAgentFileSystem } from '#/session/agentFs'; +import { IHostFileSystem } from '#/os/interface/hostFileSystem'; import { IHostEnvironment } from '#/os/interface/hostEnvironment'; import { ToolAccesses } from '#/agent/tool'; import type { BuiltinTool, ExecutableToolResult, ToolExecution } from '#/agent/tool'; @@ -136,7 +136,7 @@ export class ReadMediaFileTool implements BuiltinTool { readonly description: string; readonly parameters: Record = toInputJsonSchema(ReadMediaFileInputSchema); constructor( - private readonly fs: ISessionAgentFileSystem, + private readonly fs: IHostFileSystem, private readonly env: IHostEnvironment, private readonly workspace: WorkspaceConfig, private readonly capabilities: ModelCapability, diff --git a/packages/agent-core-v2/src/agent/plan/planService.ts b/packages/agent-core-v2/src/agent/plan/planService.ts index d90e3a7b7..98a9038fb 100644 --- a/packages/agent-core-v2/src/agent/plan/planService.ts +++ b/packages/agent-core-v2/src/agent/plan/planService.ts @@ -14,7 +14,7 @@ import { import { generateHeroSlug } from "#/_base/utils/hero-slug"; import { IAgentContextMemoryService, type ContextMessage } from '#/agent/contextMemory'; import { IAgentContextInjectorService } from '#/agent/contextInjector'; -import { ISessionAgentFileSystem } from '#/session/agentFs'; +import { IHostFileSystem } from '#/os/interface/hostFileSystem'; import { IAgentProfileService } from '#/agent/profile'; import { IAgentTelemetryContextService, ITelemetryService } from '#/app/telemetry'; import { IAgentRecordService } from '#/agent/record'; @@ -56,7 +56,7 @@ export class AgentPlanService extends Disposable implements IAgentPlanService { constructor( @IAgentContextMemoryService private readonly context: IAgentContextMemoryService, @IAgentRecordService private readonly record: IAgentRecordService, - @ISessionAgentFileSystem private readonly agentFs: ISessionAgentFileSystem, + @IHostFileSystem private readonly agentFs: IHostFileSystem, @IAgentProfileService private readonly profile: IAgentProfileService, @IAgentContextInjectorService dynamicInjector: IAgentContextInjectorService, @ITelemetryService private readonly telemetry: ITelemetryService, @@ -344,7 +344,7 @@ export class AgentPlanService extends Disposable implements IAgentPlanService { } private async ensurePlanDirectory(path: string): Promise { - await this.agentFs.mkdir(dirname(path)); + await this.agentFs.mkdir(dirname(path), { recursive: true }); } private currentCwd(): string { diff --git a/packages/agent-core-v2/src/agent/profile/context.ts b/packages/agent-core-v2/src/agent/profile/context.ts index c35f0ed11..96f640486 100644 --- a/packages/agent-core-v2/src/agent/profile/context.ts +++ b/packages/agent-core-v2/src/agent/profile/context.ts @@ -5,7 +5,7 @@ * then project-level files from the project root down to the cwd) and assembles * the {@link SystemPromptContext} bag consumed by `IAgentProfileService.useProfile`. * - * Runs on top of `ISessionAgentFileSystem` (for `readText` / `stat` / `readdir`) + * Runs on top of the os `IHostFileSystem` (for `readText` / `stat` / `readdir`) * plus the host's `homeDir` — supplied together as a small `ProfileContextDeps` * bag threaded through the helpers. * @@ -18,7 +18,7 @@ import { dirname, join, normalize } from 'pathe'; -import type { ISessionAgentFileSystem } from '#/session/agentFs'; +import type { IHostFileSystem } from '#/os/interface/hostFileSystem'; import type { SystemPromptContext } from './profile'; @@ -38,7 +38,7 @@ export const LIST_DIR_CHILD_WIDTH = 10; * the filesystem primitive plus the host home directory, not on `IKaos`. */ interface ProfileContextDeps { - readonly fs: ISessionAgentFileSystem; + readonly fs: IHostFileSystem; readonly homeDir: string; } @@ -253,7 +253,7 @@ function dedupeDirs(dirs: readonly string[]): string[] { // --------------------------------------------------------------------------- // listDirectory — compact 2-level directory tree for LLM context. // Port of v1 `packages/agent-core/src/tools/support/list-directory.ts`, driven -// through the v2 `ISessionAgentFileSystem` (`readdir` + `stat`). +// through the os `IHostFileSystem` (`readdir` + `stat`). // --------------------------------------------------------------------------- interface ListDirectoryOptions { @@ -272,16 +272,9 @@ async function collectEntries( ): Promise<{ entries: Entry[]; total: number; readable: boolean }> { const all: Entry[] = []; try { - const names = await deps.fs.readdir(dirPath); - for (const name of names) { - let isDir = false; - try { - const st = await deps.fs.stat(join(dirPath, name)); - isDir = st.isDirectory; - } catch { - // Unreadable entries keep isDir=false; still list the name. - } - all.push({ name, isDir }); + const dirents = await deps.fs.readdir(dirPath); + for (const d of dirents) { + all.push({ name: d.name, isDir: d.isDirectory }); } } catch { return { entries: [], total: 0, readable: false }; diff --git a/packages/agent-core-v2/src/agent/profile/profileService.ts b/packages/agent-core-v2/src/agent/profile/profileService.ts index d16339291..82d328314 100644 --- a/packages/agent-core-v2/src/agent/profile/profileService.ts +++ b/packages/agent-core-v2/src/agent/profile/profileService.ts @@ -28,7 +28,7 @@ import { IConfigService } from '#/app/config'; import { resolveThinkingEffort } from './thinking'; import type { LoopControl } from '#/agent/loop/configSection'; import { IHostEnvironment } from '#/os/interface/hostEnvironment'; -import { ISessionAgentFileSystem } from '#/session/agentFs'; +import { IHostFileSystem } from '#/os/interface/hostFileSystem'; import { IExecContext } from '#/session/execContext'; import { isMcpToolName } from '#/agent/tool'; import { ISessionWorkspaceContext } from '#/session/workspaceContext'; @@ -81,7 +81,7 @@ export class AgentProfileService implements IAgentProfileService { @IConfigService private readonly config: IConfigService, @IModelResolver private readonly modelFactory: IModelResolver, @IHostEnvironment private readonly env: IHostEnvironment, - @ISessionAgentFileSystem private readonly fs: ISessionAgentFileSystem, + @IHostFileSystem private readonly fs: IHostFileSystem, @IExecContext private readonly execCtx: IExecContext, @IBootstrapService private readonly bootstrap: IBootstrapService, @ISessionWorkspaceContext private readonly workspace: ISessionWorkspaceContext, diff --git a/packages/agent-core-v2/src/os/backends/node-local/hostFsService.ts b/packages/agent-core-v2/src/os/backends/node-local/hostFsService.ts index 5a7f1c6c1..0f8dda28a 100644 --- a/packages/agent-core-v2/src/os/backends/node-local/hostFsService.ts +++ b/packages/agent-core-v2/src/os/backends/node-local/hostFsService.ts @@ -36,8 +36,16 @@ function* splitLinesKeepingTerminator(text: string): Generator { export class HostFileSystem implements IHostFileSystem { declare readonly _serviceBrand: undefined; - async readText(path: string): Promise { - return readFile(path, 'utf8'); + async readText( + path: string, + options?: { encoding?: BufferEncoding; errors?: TextDecodeErrors }, + ): Promise { + if (options === undefined) { + return readFile(path, 'utf8'); + } + const encoding = options.encoding ?? 'utf-8'; + const errors = options.errors ?? 'strict'; + return decodeTextWithErrors(await readFile(path), encoding, errors); } async writeText(path: string, data: string): Promise { @@ -142,7 +150,13 @@ export class HostFileSystem implements IHostFileSystem { async stat(path: string): Promise { const s = await stat(path); - return { isFile: s.isFile(), isDirectory: s.isDirectory(), size: s.size }; + return { + isFile: s.isFile(), + isDirectory: s.isDirectory(), + size: s.size, + mtimeMs: s.mtimeMs, + ino: s.ino, + }; } async readdir(path: string): Promise { diff --git a/packages/agent-core-v2/src/os/interface/hostFileSystem.ts b/packages/agent-core-v2/src/os/interface/hostFileSystem.ts index 4265835a1..f6a49312c 100644 --- a/packages/agent-core-v2/src/os/interface/hostFileSystem.ts +++ b/packages/agent-core-v2/src/os/interface/hostFileSystem.ts @@ -14,6 +14,10 @@ export interface HostFileStat { readonly isFile: boolean; readonly isDirectory: boolean; readonly size: number; + /** Last-modified time in epoch milliseconds, when the backend exposes it. */ + readonly mtimeMs?: number; + /** Inode number, when the backend exposes it (`0` on backends without inodes). */ + readonly ino?: number; } export interface HostDirEntry { @@ -25,7 +29,10 @@ export interface HostDirEntry { export interface IHostFileSystem { readonly _serviceBrand: undefined; - readText(path: string): Promise; + readText( + path: string, + options?: { encoding?: BufferEncoding; errors?: TextDecodeErrors }, + ): Promise; writeText(path: string, data: string): Promise; /** * Read bytes from `path`. When `n` is given, reads at most the first `n` diff --git a/packages/agent-core-v2/src/session/agentFs/agentFsService.ts b/packages/agent-core-v2/src/session/agentFs/agentFsService.ts deleted file mode 100644 index 73e916aee..000000000 --- a/packages/agent-core-v2/src/session/agentFs/agentFsService.ts +++ /dev/null @@ -1,433 +0,0 @@ -/** - * `agentFs` domain (L2) — `ISessionAgentFileSystem` implementation. - * - * Focused file-IO surface implemented directly on Node's `fs/promises`. - * Relative paths are resolved against `IExecContext.cwd`; `glob` uses the - * vendored `_globWalk` traversal (with (dev, ino) cycle detection tailored - * around Windows FAT/exFAT inode-less filesystems). No `IKaos` dependency — - * `withCwd` derives a fresh instance around `IExecContext.withCwd(cwd)`. - * Bound at Session scope. - */ - -import { mkdir, open, readdir, readFile, stat, writeFile, appendFile } from 'node:fs/promises'; -import { isAbsolute, join, normalize } from 'pathe'; - -import { InstantiationType } from '#/_base/di/extensions'; -import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; -import { - decodeTextWithErrors, - globPatternToRegex, - type TextDecodeErrors, -} from '#/_base/execEnv'; -import { ErrorCodes, KimiError } from '#/errors'; -import { IExecContext } from '#/session/execContext'; - -import { type AgentFileStat, ISessionAgentFileSystem } from './fileSystem'; - -const READ_CHUNK_SIZE = 64 * 1024; - -/** - * Build the `(dev, ino)` cycle-detection key used by `_globWalk`'s - * visited set. Returns `null` when `ino` is 0, which Node returns on - * filesystems that don't carry inodes (Windows FAT/exFAT, some SMB/NFS - * mounts). A null key signals "no reliable identity for this dir" so - * the caller skips visited tracking for that descent — cycle safety - * is weakened on those filesystems, but normal walking works instead - * of every directory colliding on the shared key `":0"`. - */ -function cycleKey(s: { dev: number; ino: number }): string | null { - if (s.ino === 0) return null; - return `${String(s.dev)}:${String(s.ino)}`; -} - -function isUtf8Encoding(encoding: BufferEncoding): boolean { - return encoding === 'utf-8' || encoding === 'utf8'; -} - -function* splitLinesKeepingTerminator(text: string): Generator { - if (text.length === 0) return; - let start = 0; - for (let i = 0; i < text.length; i += 1) { - if (text.codePointAt(i) === 0x0a) { - yield text.slice(start, i + 1); - start = i + 1; - } - } - if (start < text.length) { - yield text.slice(start); - } -} - -export class SessionAgentFileSystem implements ISessionAgentFileSystem { - declare readonly _serviceBrand: undefined; - - constructor(@IExecContext private readonly ctx: IExecContext) {} - - get cwd(): string { - return this.ctx.cwd; - } - - private _resolvePath(path: string): string { - if (isAbsolute(path)) return normalize(path); - return join(this.ctx.cwd, path); - } - - async readText( - path: string, - options?: { encoding?: BufferEncoding; errors?: TextDecodeErrors }, - ): Promise { - const resolved = this._resolvePath(path); - const encoding = options?.encoding ?? 'utf-8'; - const errors = options?.errors ?? 'strict'; - const data = await readFile(resolved); - return decodeTextWithErrors(data, encoding, errors); - } - - async writeText( - path: string, - data: string, - options?: { mode?: 'w' | 'a'; encoding?: BufferEncoding }, - ): Promise { - const resolved = this._resolvePath(path); - const encoding = options?.encoding ?? 'utf-8'; - const mode = options?.mode ?? 'w'; - if (mode === 'a') { - await appendFile(resolved, data, encoding); - } else { - await writeFile(resolved, data, encoding); - } - } - - async readBytes(path: string, n?: number): Promise { - const resolved = this._resolvePath(path); - if (n === undefined) { - return Buffer.from(await readFile(resolved)); - } - const fh = await open(resolved, 'r'); - try { - const buf = Buffer.alloc(n); - const { bytesRead } = await fh.read(buf, 0, n, 0); - return buf.subarray(0, bytesRead); - } finally { - await fh.close(); - } - } - - async *readLines( - path: string, - options?: { encoding?: BufferEncoding; errors?: TextDecodeErrors }, - ): AsyncGenerator { - const resolved = this._resolvePath(path); - const encoding = options?.encoding ?? 'utf-8'; - const errors = options?.errors ?? 'strict'; - - if (!isUtf8Encoding(encoding)) { - const content = decodeTextWithErrors(await readFile(resolved), encoding, errors); - yield* splitLinesKeepingTerminator(content); - return; - } - - yield* this._readUtf8Lines(resolved, errors); - } - - private async *_readUtf8Lines( - resolved: string, - errors: TextDecodeErrors, - ): AsyncGenerator { - const fh = await open(resolved, 'r'); - try { - const buf = Buffer.alloc(READ_CHUNK_SIZE); - let pending: Buffer[] = []; - let pendingOffset = 0; - let fileOffset = 0; - - while (true) { - const { bytesRead } = await fh.read(buf, 0, buf.length, null); - if (bytesRead === 0) break; - const chunk = buf.subarray(0, bytesRead); - let lineStart = 0; - - for (let i = 0; i < chunk.length; i += 1) { - const byte = chunk[i]; - if (byte !== 0x0a) continue; - const piece = chunk.subarray(lineStart, i + 1); - const lineOffset = pending.length === 0 ? fileOffset + lineStart : pendingOffset; - const line = pending.length === 0 ? piece : Buffer.concat([...pending, piece]); - yield decodeTextWithErrors(line, 'utf-8', errors, lineOffset !== 0); - pending = []; - lineStart = i + 1; - } - - if (lineStart < chunk.length) { - const tail = Buffer.from(chunk.subarray(lineStart)); - if (pending.length === 0) pendingOffset = fileOffset + lineStart; - pending.push(tail); - } - fileOffset += bytesRead; - } - - if (pending.length > 0) { - const line = Buffer.concat(pending); - yield decodeTextWithErrors(line, 'utf-8', errors, pendingOffset !== 0); - } - } finally { - await fh.close(); - } - } - - async writeBytes(path: string, data: Uint8Array): Promise { - const resolved = this._resolvePath(path); - await writeFile(resolved, data); - } - - async stat(path: string): Promise { - const resolved = this._resolvePath(path); - // The public interface has no `followSymlinks` toggle; always follow - // symlinks (matching the previous `IKaos.backend.stat` default). - const s = await stat(resolved); - return { - isFile: s.isFile(), - isDirectory: s.isDirectory(), - size: s.size, - mtimeMs: s.mtimeMs, - ino: s.ino, - }; - } - - async readdir(path: string): Promise { - const resolved = this._resolvePath(path); - return await readdir(resolved); - } - - async glob(pattern: string): Promise { - const resolved = this._resolvePath('.'); - const caseSensitive = true; - const patternParts = pattern.split('/'); - // Seed `visited` with basePath's own inode so that a symlink inside - // basePath that points back at basePath is caught on its first - // encounter (not on the second level — the "+1 depth" off-by-one - // that would otherwise leak if the caller globs directly from the - // loop root). `stat` failure here is tolerated: `_globWalk` will - // hit the same error via readdir and return empty. - const initVisited = new Set(); - try { - const rootStat = await stat(resolved); - const rootKey = cycleKey(rootStat); - if (rootKey !== null) initVisited.add(rootKey); - } catch { - // base does not exist / not accessible — walker handles via its own catch - } - const out: string[] = []; - for await (const match of this._globWalk(resolved, patternParts, caseSensitive, initVisited)) { - out.push(match); - } - return out; - } - - // `visited` holds the `(stDev, stIno)` keys of directories on the - // current descent path. Before recursing into a subdirectory, we - // check its key against `visited`; if present we skip it (cycle - // detected) and otherwise recurse with a fresh Set containing the - // additional key. The per-recurse copy gives the check path-local - // semantics: two legitimate symlinks to the same target in separate - // branches both traverse, which is more permissive than Python stdlib - // while still cycle-safe. - // Same-directory self-recursion (e.g. `**` matching zero dirs with - // pattern tail) passes `visited` unchanged — no descent, no cycle - // risk. - // - // Windows note: Node's `fs.Stats.ino` returns `0` on filesystems - // that don't support inodes (FAT/exFAT, some SMB/NFS mounts). If we - // keyed on `ino=0`, every directory on such a drive would share the - // key `":0"` and the first would "visit" all others. The - // module-level `cycleKey` helper returns `null` in that case, which - // causes the call sites to skip visited tracking for that descent - // — cycle safety is lost on those filesystems, but normal walking - // works. - private async *_globWalk( - basePath: string, - patternParts: string[], - caseSensitive: boolean, - visited: Set, - ): AsyncGenerator { - if (patternParts.length === 0) { - return; - } - - const [currentPattern, ...remainingParts] = patternParts; - - if (currentPattern === '**') { - // `**` matches zero or more directory components. - // - // There are exactly two cases to handle: - // (a) `**` matches zero directories → continue at basePath with the - // remaining pattern parts (or yield basePath itself when `**` - // is the final segment). - // (b) `**` matches one or more directories → recurse into each - // subdirectory, keeping `**` (i.e. the full patternParts) at - // the front. The "zero directories" case is then re-evaluated - // at the subdirectory level by that recursive call. - // - // We must NOT additionally recurse with `remainingParts` on - // subdirectories — that would double-count every match at depth ≥ 1 - // because case (a) inside the child recursion already yields those - // results. - if (remainingParts.length > 0) { - yield* this._globWalk(basePath, remainingParts, caseSensitive, visited); - } else { - // Pattern ends with `**`: yield basePath itself (zero-dir match). - yield basePath; - } - - let entries: string[]; - try { - entries = await readdir(basePath); - } catch { - return; - } - - for (const entry of entries) { - // Use join to avoid "//entry" when basePath is a filesystem root. - const fullPath = join(basePath, entry); - let entryStat; - try { - entryStat = await stat(fullPath); - } catch { - continue; - } - if (entryStat.isDirectory()) { - const key = cycleKey(entryStat); - if (key !== null && visited.has(key)) continue; - yield* this._globWalk( - fullPath, - patternParts, - caseSensitive, - key !== null ? new Set([...visited, key]) : visited, - ); - } else if (remainingParts.length === 0) { - // Pattern ends with `**`: non-directory entries match too - // (since `**` matches "anything"). - yield fullPath; - } - } - } else { - const regex = globPatternToRegex(currentPattern ?? '', caseSensitive); - - let entries: string[]; - try { - entries = await readdir(basePath); - } catch { - return; - } - - for (const entry of entries) { - if (!regex.test(entry)) { - continue; - } - - // Use join to avoid "//entry" when basePath is a filesystem root. - const fullPath = join(basePath, entry); - - if (remainingParts.length === 0) { - yield fullPath; - } else { - let entryStat; - try { - entryStat = await stat(fullPath); - } catch { - continue; - } - if (entryStat.isDirectory()) { - const key = cycleKey(entryStat); - if (key !== null && visited.has(key)) continue; - yield* this._globWalk( - fullPath, - remainingParts, - caseSensitive, - key !== null ? new Set([...visited, key]) : visited, - ); - } - } - } - } - } - - async mkdir( - path: string, - options?: { readonly parents?: boolean; readonly existOk?: boolean }, - ): Promise { - const resolved = this._resolvePath(path); - const parents = options?.parents ?? true; - const existOk = options?.existOk ?? true; - - if (parents) { - // `fs.mkdir(..., { recursive: true })` silently succeeds when the - // target already exists — it does NOT raise EEXIST. To honor the - // `existOk: false` semantics, we must probe for existence ourselves - // before delegating to the recursive mkdir. - if (!existOk) { - try { - const s = await stat(resolved); - if (s.isDirectory()) { - throw new KimiError( - ErrorCodes.FS_ALREADY_EXISTS, - `${resolved} already exists`, - ); - } - // Path exists but is not a directory — let `mkdir` surface the - // appropriate error (EEXIST/ENOTDIR) below. - } catch (error: unknown) { - if (error instanceof KimiError) throw error; - const err = error as NodeJS.ErrnoException; - if (err.code !== 'ENOENT') throw error; - // ENOENT: target doesn't exist yet — proceed to mkdir. - } - } - await mkdir(resolved, { recursive: true }); - return; - } - - // Non-recursive: fs.mkdir naturally throws EEXIST on collision. - try { - await mkdir(resolved); - } catch (error: unknown) { - if ( - existOk && - error instanceof Error && - 'code' in error && - (error as NodeJS.ErrnoException).code === 'EEXIST' - ) { - // `existOk` only applies when the conflicting path is itself a - // directory. If a regular file (or other non-directory) already - // occupies the path, silently returning would be a lie — the - // requested directory still does not exist. Surface the conflict - // explicitly so callers cannot mistake "file collision" for - // "directory already present". - const s = await stat(resolved); - if (!s.isDirectory()) { - throw new KimiError( - ErrorCodes.FS_ALREADY_EXISTS, - `${resolved} already exists but is not a directory`, - ); - } - return; - } - throw error; - } - } - - withCwd(cwd: string): ISessionAgentFileSystem { - // DI bypass: `withCwd` returns a fresh immutable value on top of the - // derived `IExecContext`, mirroring the pre-refactor pattern - // (`new SessionAgentFileSystem(this.kaos.withCwd(cwd))`). - return new SessionAgentFileSystem(this.ctx.withCwd(cwd)); - } -} - -registerScopedService( - LifecycleScope.Session, - ISessionAgentFileSystem, - SessionAgentFileSystem, - InstantiationType.Delayed, - 'agentFs', -); diff --git a/packages/agent-core-v2/src/session/agentFs/fileSystem.ts b/packages/agent-core-v2/src/session/agentFs/fileSystem.ts deleted file mode 100644 index a477a4a1d..000000000 --- a/packages/agent-core-v2/src/session/agentFs/fileSystem.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * `agentFs` domain (L2) — the Agent's filesystem. - * - * Defines the `ISessionAgentFileSystem` that business code injects to read and - * write files inside the Agent's execution environment. Session-scoped; the - * implementation resolves relative paths against `IExecContext.cwd` and talks - * to Node's `fs/promises` directly. - */ - -import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation'; -import type { TextDecodeErrors } from '#/_base/execEnv'; - -export interface AgentFileStat { - readonly isFile: boolean; - readonly isDirectory: boolean; - readonly size: number; - /** Last-modified time in epoch milliseconds, when the backend exposes it. */ - readonly mtimeMs?: number; - /** Inode number, when the backend exposes it (`0` on backends without inodes). */ - readonly ino?: number; -} - -export interface ISessionAgentFileSystem { - readonly _serviceBrand: undefined; - - readonly cwd: string; - - readText( - path: string, - options?: { encoding?: BufferEncoding; errors?: TextDecodeErrors }, - ): Promise; - writeText( - path: string, - data: string, - options?: { mode?: 'w' | 'a'; encoding?: BufferEncoding }, - ): Promise; - readBytes(path: string, n?: number): Promise; - readLines( - path: string, - options?: { encoding?: BufferEncoding; errors?: TextDecodeErrors }, - ): AsyncGenerator; - writeBytes(path: string, data: Uint8Array): Promise; - stat(path: string): Promise; - readdir(path: string): Promise; - glob(pattern: string): Promise; - mkdir( - path: string, - options?: { readonly parents?: boolean; readonly existOk?: boolean }, - ): Promise; - withCwd(cwd: string): ISessionAgentFileSystem; -} - -export const ISessionAgentFileSystem: ServiceIdentifier = - createDecorator('sessionAgentFileSystem'); diff --git a/packages/agent-core-v2/src/session/agentFs/fs.ts b/packages/agent-core-v2/src/session/agentFs/fs.ts index c6a1ce281..0231e6d34 100644 --- a/packages/agent-core-v2/src/session/agentFs/fs.ts +++ b/packages/agent-core-v2/src/session/agentFs/fs.ts @@ -2,11 +2,11 @@ * `agentFs` domain (L2) — wire-shaped filesystem operations. * * Defines the `ISessionFsService` that backs the fs REST surface: content search, - * content grep, and git status/diff. It is the higher-level counterpart to - * `ISessionAgentFileSystem` (the thin IO primitive): it orchestrates the IO primitive - * plus `ISessionProcessRunner` (for `rg` / `git` / `gh`) and returns protocol-shaped - * responses. Session-scoped — the scope itself is the session, so no - * `sessionId` is threaded through. + * content grep, and git status/diff. It orchestrates the os `IHostFileSystem` + * (file IO, resolved against the workspace root) plus `ISessionProcessRunner` + * (for `rg` / `git` / `gh`) and returns protocol-shaped responses. + * Session-scoped — the scope itself is the session, so no `sessionId` is + * threaded through. */ import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation'; diff --git a/packages/agent-core-v2/src/session/agentFs/fsService.ts b/packages/agent-core-v2/src/session/agentFs/fsService.ts index c1a862b8c..eebb6562a 100644 --- a/packages/agent-core-v2/src/session/agentFs/fsService.ts +++ b/packages/agent-core-v2/src/session/agentFs/fsService.ts @@ -2,8 +2,8 @@ * `agentFs` domain (L2) — `ISessionFsService` implementation. * * Backs the fs REST surface (search / grep / git status / git diff) by - * orchestrating `ISessionAgentFileSystem` (file IO), `ISessionProcessRunner` (`rg`), - * and `IGitService` (git status / diff). Bound at Session scope — the workspace + * orchestrating the os `IHostFileSystem` (file IO, resolved against the + * workspace root), `ISessionProcessRunner` (`rg`), and `IGitService` (git * root and execution environment come from the scope, so no `sessionId` is * threaded through. Git operations are delegated to the App-scoped * `IGitService`; this service only confines paths and computes repo-relative @@ -13,7 +13,7 @@ * follow symlinks, matching the rest of v2 (`_base/tools/policies/path-access.ts`). */ -import { basename, extname, isAbsolute, relative, sep } from 'node:path'; +import { basename, extname, isAbsolute, join, relative, sep } from 'node:path'; import { ErrorCode, @@ -49,10 +49,10 @@ import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; import { ErrorCodes, KimiError } from '#/errors'; import { IGitService } from '#/app/git'; import { ITelemetryService } from '#/app/telemetry'; +import { IHostFileSystem, type HostFileStat } from '#/os/interface/hostFileSystem'; import { ISessionProcessRunner } from '#/session/process'; import { ISessionWorkspaceContext } from '#/session/workspaceContext'; -import { type AgentFileStat, ISessionAgentFileSystem } from './fileSystem'; import { type FsDownloadResolved, type FsPathResolved, ISessionFsService } from './fs'; import { runCommand } from './fsProcess'; import { ensureRgPath, type RgProbe, type RgResolution } from './rgLocator'; @@ -94,19 +94,24 @@ export class SessionFsService implements ISessionFsService { constructor( @ISessionWorkspaceContext private readonly workspace: ISessionWorkspaceContext, - @ISessionAgentFileSystem private readonly fs: ISessionAgentFileSystem, + @IHostFileSystem private readonly hostFs: IHostFileSystem, @ISessionProcessRunner private readonly runner: ISessionProcessRunner, @ITelemetryService private readonly telemetry: ITelemetryService, @IGitService private readonly git: IGitService, ) {} + /** Resolve a workspace-relative path (or `.`) back to an absolute path for `IHostFileSystem`. */ + private absOf(rel: string): string { + return rel === '' || rel === '.' ? this.workspace.workDir : join(this.workspace.workDir, rel); + } + async list(req: FsListRequest): Promise { const abs = this.resolveWithin(req.path); const rel = this.toRel(abs); - let topStat: AgentFileStat; + let topStat: HostFileStat; try { - topStat = await this.fs.stat(rel); + topStat = await this.hostFs.stat(abs); } catch (err) { throw mapFsError(err, req.path); } @@ -133,14 +138,14 @@ export class SessionFsService implements ISessionFsService { interface Child { readonly name: string; readonly relPath: string; - readonly stat: AgentFileStat; + readonly stat: HostFileStat; } while (queue.length > 0) { const entry = queue.shift()!; let names: readonly string[]; try { - names = await this.fs.readdir(entry.relPath === '' ? '.' : entry.relPath); + names = (await this.hostFs.readdir(this.absOf(entry.relPath))).map((e) => e.name); } catch (err) { if (entry.relPath === (rel === '.' ? '' : rel)) { throw mapFsError(err, req.path); @@ -156,7 +161,7 @@ export class SessionFsService implements ISessionFsService { continue; } if (req.exclude_globs && matchesAnyGlob(childRel, req.exclude_globs)) continue; - const st = await this.fs.stat(childRel).catch(() => undefined); + const st = await this.hostFs.stat(this.absOf(childRel)).catch(() => undefined); if (st === undefined) continue; visible.push({ name, relPath: childRel, stat: st }); } @@ -196,9 +201,9 @@ export class SessionFsService implements ISessionFsService { const abs = this.resolveWithin(req.path); const rel = this.toRel(abs); - let st: AgentFileStat; + let st: HostFileStat; try { - st = await this.fs.stat(rel); + st = await this.hostFs.stat(abs); } catch (err) { throw mapFsError(err, req.path); } @@ -217,7 +222,7 @@ export class SessionFsService implements ISessionFsService { const sampleSize = Math.min(FS_BINARY_SAMPLE_BYTES, st.size); const sample = - sampleSize === 0 ? new Uint8Array() : await this.fs.readBytes(rel, sampleSize); + sampleSize === 0 ? new Uint8Array() : await this.hostFs.readBytes(abs, sampleSize); const isBinary = detectBinary(sample); if (isBinary && req.encoding === 'utf-8') { @@ -231,7 +236,7 @@ export class SessionFsService implements ISessionFsService { if (effectiveLength <= 0) { bytes = new Uint8Array(); } else { - const window = await this.fs.readBytes(rel, req.offset + effectiveLength); + const window = await this.hostFs.readBytes(abs, req.offset + effectiveLength); bytes = window.subarray(req.offset, req.offset + effectiveLength); } @@ -295,9 +300,9 @@ export class SessionFsService implements ISessionFsService { async stat(req: FsStatRequest): Promise { const abs = this.resolveWithin(req.path); const rel = this.toRel(abs); - let st: AgentFileStat; + let st: HostFileStat; try { - st = await this.fs.stat(rel); + st = await this.hostFs.stat(abs); } catch (err) { throw mapFsError(err, req.path); } @@ -315,7 +320,7 @@ export class SessionFsService implements ISessionFsService { await Promise.all( resolved.map(async ({ raw, rel, abs }) => { try { - const st = await this.fs.stat(rel); + const st = await this.hostFs.stat(abs); const name = rel === '.' ? basename(this.workspace.workDir) : basename(abs); entries[raw] = buildFsEntry(rel, name, st, false); } catch { @@ -330,7 +335,7 @@ export class SessionFsService implements ISessionFsService { const abs = this.resolveWithin(req.path); const rel = this.toRel(abs); try { - await this.fs.mkdir(rel, { parents: req.recursive, existOk: req.recursive }); + await this.hostFs.mkdir(abs, { recursive: req.recursive }); } catch (err) { const code = errnoCode(err); if (code === 'EEXIST') { @@ -345,16 +350,16 @@ export class SessionFsService implements ISessionFsService { } throw err; } - const st = await this.fs.stat(rel); + const st = await this.hostFs.stat(abs); return buildFsEntry(rel, basename(abs), st, false); } async resolvePath(relPath: string): Promise { const abs = this.resolveWithin(relPath); const rel = this.toRel(abs); - let st: AgentFileStat; + let st: HostFileStat; try { - st = await this.fs.stat(rel); + st = await this.hostFs.stat(abs); } catch (err) { throw mapFsError(err, relPath); } @@ -364,9 +369,9 @@ export class SessionFsService implements ISessionFsService { async resolveDownload(relPath: string): Promise { const abs = this.resolveWithin(relPath); const rel = this.toRel(abs); - let st: AgentFileStat; + let st: HostFileStat; try { - st = await this.fs.stat(rel); + st = await this.hostFs.stat(abs); } catch (err) { throw mapFsError(err, relPath); } @@ -377,7 +382,7 @@ export class SessionFsService implements ISessionFsService { } const sampleSize = Math.min(FS_BINARY_SAMPLE_BYTES, st.size); const sample = - sampleSize === 0 ? new Uint8Array() : await this.fs.readBytes(rel, sampleSize); + sampleSize === 0 ? new Uint8Array() : await this.hostFs.readBytes(abs, sampleSize); const isBinary = detectBinary(sample); return { absolute: abs, @@ -530,7 +535,7 @@ export class SessionFsService implements ISessionFsService { filesScanned += 1; let content: string; try { - content = await this.fs.readText(rel); + content = await this.hostFs.readText(this.absOf(rel)); } catch { continue; } @@ -579,14 +584,14 @@ export class SessionFsService implements ISessionFsService { if (depth > WALK_MAX_DEPTH) return; let names: readonly string[]; try { - names = await this.fs.readdir(rootRel === '' ? '.' : rootRel); + names = (await this.hostFs.readdir(this.absOf(rootRel))).map((e) => e.name); } catch { return; } for (const name of names) { if (name === '.git') continue; const childRel = rootRel === '' ? name : `${rootRel}/${name}`; - const st = await this.fs.stat(childRel).catch(() => undefined); + const st = await this.hostFs.stat(this.absOf(childRel)).catch(() => undefined); if (st === undefined) continue; const isDir = st.isDirectory; if (matcher) { @@ -608,7 +613,7 @@ export class SessionFsService implements ISessionFsService { const ig = ignore(); ig.add('.git/'); try { - const contents = await this.fs.readText('.gitignore'); + const contents = await this.hostFs.readText(join(this.workspace.workDir, '.gitignore')); ig.add(contents); } catch { // No .gitignore — keep the `.git/` default only. @@ -778,11 +783,11 @@ function isHidden(name: string): boolean { } function sortChildren( - children: { name: string; stat: AgentFileStat }[], + children: { name: string; stat: HostFileStat }[], sort: FsListRequest['sort'], ): void { const cmp = { - type_first: (a: { name: string; stat: AgentFileStat }, b: { name: string; stat: AgentFileStat }) => { + type_first: (a: { name: string; stat: HostFileStat }, b: { name: string; stat: HostFileStat }) => { const ad = a.stat.isDirectory ? 0 : 1; const bd = b.stat.isDirectory ? 0 : 1; if (ad !== bd) return ad - bd; @@ -797,7 +802,7 @@ function sortChildren( children.sort(cmp); } -function buildEtag(st: AgentFileStat): string { +function buildEtag(st: HostFileStat): string { const mtime = Math.floor(st.mtimeMs ?? 0); const ino = st.ino ?? 0; return [mtime.toString(36), st.size.toString(36), ino.toString(36)].join('-'); @@ -806,7 +811,7 @@ function buildEtag(st: AgentFileStat): string { function buildFsEntry( relPath: string, name: string, - st: AgentFileStat, + st: HostFileStat, withMime: boolean, ): FsEntry { const kind: FsEntry['kind'] = st.isDirectory ? 'directory' : 'file'; diff --git a/packages/agent-core-v2/src/session/agentFs/index.ts b/packages/agent-core-v2/src/session/agentFs/index.ts index 59fdc3ed8..c5815b56c 100644 --- a/packages/agent-core-v2/src/session/agentFs/index.ts +++ b/packages/agent-core-v2/src/session/agentFs/index.ts @@ -1,10 +1,8 @@ /** - * `agentFs` domain barrel — re-exports the agent-filesystem contract - * and its node-local backend, plus the session-level facade files. + * `agentFs` domain barrel — re-exports the session fs facade (`ISessionFsService`) + * and its helpers. */ -export * from './fileSystem'; -export * from './agentFsService'; export * from './errors'; export * from './fs'; export * from './fsService'; diff --git a/packages/agent-core-v2/src/session/session/sessionWarningService.ts b/packages/agent-core-v2/src/session/session/sessionWarningService.ts index d5ec18af6..7249f16d0 100644 --- a/packages/agent-core-v2/src/session/session/sessionWarningService.ts +++ b/packages/agent-core-v2/src/session/session/sessionWarningService.ts @@ -16,7 +16,7 @@ import { LifecycleScope, registerScopedService } from '#/_base/di/scope'; import { IAgentLifecycleService } from '#/session/agentLifecycle'; import { IBootstrapService } from '#/app/bootstrap'; import { IHostEnvironment } from '#/os/interface/hostEnvironment'; -import { ISessionAgentFileSystem } from '#/session/agentFs'; +import { IHostFileSystem } from '#/os/interface/hostFileSystem'; import { IExecContext } from '#/session/execContext'; import { IAgentProfileService, prepareSystemPromptContext } from '#/agent/profile'; import { ISessionWorkspaceContext } from '#/session/workspaceContext'; @@ -31,7 +31,7 @@ export class SessionWarningService implements ISessionWarningService { constructor( @IHostEnvironment private readonly env: IHostEnvironment, - @ISessionAgentFileSystem private readonly fs: ISessionAgentFileSystem, + @IHostFileSystem private readonly fs: IHostFileSystem, @IExecContext private readonly ctx: IExecContext, @IBootstrapService private readonly bootstrap: IBootstrapService, @ISessionWorkspaceContext private readonly workspace: ISessionWorkspaceContext, diff --git a/packages/agent-core-v2/test/agentFs/agentFsService.test.ts b/packages/agent-core-v2/test/agentFs/agentFsService.test.ts deleted file mode 100644 index 0bee20b58..000000000 --- a/packages/agent-core-v2/test/agentFs/agentFsService.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; - -import { mkdtemp, rm } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; - -import { InstantiationType } from '#/_base/di/extensions'; -import { - LifecycleScope, - _clearScopedRegistryForTests, - registerScopedService, -} from '#/_base/di/scope'; -import { createScopedTestHost, stubPair } from '#/_base/di/test'; -import { SessionAgentFileSystem, ISessionAgentFileSystem } from '#/session/agentFs'; -import { IExecContext, createExecContext } from '#/session/execContext'; - -describe('SessionAgentFileSystem (backed by IExecContext)', () => { - let dir: string; - - beforeEach(async () => { - _clearScopedRegistryForTests(); - registerScopedService( - LifecycleScope.Session, - ISessionAgentFileSystem, - SessionAgentFileSystem, - InstantiationType.Delayed, - 'agentFs', - ); - dir = await mkdtemp(join(tmpdir(), 'agentfs-')); - }); - - afterEach(async () => { - await rm(dir, { recursive: true, force: true }); - }); - - async function makeFs(): Promise { - const host = createScopedTestHost(); - const session = host.child( - LifecycleScope.Session, - 's', - [stubPair(IExecContext, createExecContext(dir))], - ); - return session.accessor.get(ISessionAgentFileSystem); - } - - it('writes and reads text relative to cwd', async () => { - const fs = await makeFs(); - await fs.writeText('a.txt', 'hello'); - expect(await fs.readText('a.txt')).toBe('hello'); - }); - - it('stat reports file kind and byte size', async () => { - const fs = await makeFs(); - await fs.writeText('b.txt', 'abc'); - const st = await fs.stat('b.txt'); - expect(st.isFile).toBe(true); - expect(st.isDirectory).toBe(false); - expect(st.size).toBe(3); - }); - - it('stat reports directories', async () => { - const fs = await makeFs(); - await fs.mkdir('sub'); - const st = await fs.stat('sub'); - expect(st.isDirectory).toBe(true); - expect(st.isFile).toBe(false); - }); - - it('readdir returns entry names', async () => { - const fs = await makeFs(); - await fs.writeText('x.txt', ''); - await fs.mkdir('sub'); - const names = [...(await fs.readdir('.'))].sort(); - expect(names).toEqual(['sub', 'x.txt']); - }); - - it('withCwd derives a sub-view rooted at the new cwd', async () => { - const fs = await makeFs(); - await fs.mkdir('sub'); - await fs.writeText('sub/c.txt', 'deep'); - const sub = fs.withCwd(join(dir, 'sub')); - expect(sub.cwd).toBe(join(dir, 'sub')); - expect(await sub.readText('c.txt')).toBe('deep'); - }); -}); diff --git a/packages/agent-core-v2/test/agentFs/fsService.test.ts b/packages/agent-core-v2/test/agentFs/fsService.test.ts index c7835987f..cfeeb5e73 100644 --- a/packages/agent-core-v2/test/agentFs/fsService.test.ts +++ b/packages/agent-core-v2/test/agentFs/fsService.test.ts @@ -1,4 +1,4 @@ -import { isAbsolute, relative, resolve } from 'node:path'; +import { isAbsolute, join, relative, resolve } from 'node:path'; import { Readable, Writable } from 'node:stream'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; @@ -12,7 +12,7 @@ import { import { createScopedTestHost, stubPair } from '#/_base/di/test'; import { IGitService } from '#/app/git'; import { ErrorCodes, KimiError } from '#/errors'; -import { ISessionAgentFileSystem } from '#/session/agentFs'; +import { type HostDirEntry, IHostFileSystem } from '#/os/interface/hostFileSystem'; import { ISessionFsService } from '#/session/agentFs/fs'; import { SessionFsService } from '#/session/agentFs/fsService'; import { ISessionProcessRunner, type IProcess } from '#/session/process'; @@ -38,16 +38,20 @@ function stubWorkspace(): ISessionWorkspaceContext { }; } -function fakeFs(files: Record): ISessionAgentFileSystem { - const fileMap = new Map(Object.entries(files)); - const dirSet = new Set(['']); - for (const p of fileMap.keys()) { - const parts = p.split('/'); +function fakeFs(files: Record): IHostFileSystem { + // Keys are stored as absolute paths; fsService now resolves workspace-relative + // paths to absolute (`join(WORK_DIR, rel)`) before calling into `IHostFileSystem`. + const fileMap = new Map(); + const dirSet = new Set([WORK_DIR]); + for (const [rel, content] of Object.entries(files)) { + const abs = join(WORK_DIR, rel); + fileMap.set(abs, content); + const parts = rel.split('/'); for (let i = 1; i < parts.length; i++) { - dirSet.add(parts.slice(0, i).join('/')); + dirSet.add(join(WORK_DIR, parts.slice(0, i).join('/'))); } } - const isDir = (p: string): boolean => p === '' || p === '.' || dirSet.has(p); + const isDir = (p: string): boolean => p === WORK_DIR || dirSet.has(p); const enoent = (p: string): NodeJS.ErrnoException => { const err = new Error(`ENOENT: ${p}`) as NodeJS.ErrnoException; err.code = 'ENOENT'; @@ -55,7 +59,6 @@ function fakeFs(files: Record): ISessionAgentFileSystem { }; return { _serviceBrand: undefined, - cwd: WORK_DIR, readText: async (p) => { const c = fileMap.get(p); if (c === undefined) throw enoent(p); @@ -72,6 +75,7 @@ function fakeFs(files: Record): ISessionAgentFileSystem { // not needed by the fs surface under test }, writeBytes: async () => {}, + createExclusive: async () => false, stat: async (p) => { if (fileMap.has(p)) { return { @@ -88,41 +92,62 @@ function fakeFs(files: Record): ISessionAgentFileSystem { throw enoent(p); }, readdir: async (p) => { - const prefix = p === '.' || p === '' ? '' : `${p}/`; - const children = new Set(); - const addChild = (key: string): void => { - if (key === '' || key === p) return; - if (!key.startsWith(prefix)) return; + if (!isDir(p)) throw enoent(p); + const prefix = `${p}/`; + const children = new Map(); + const addDir = (name: string): void => { + if (!children.has(name)) { + children.set(name, { name, isFile: false, isDirectory: true }); + } + }; + const addFile = (name: string): void => { + if (!children.has(name)) { + children.set(name, { name, isFile: true, isDirectory: false }); + } + }; + const visit = (key: string, isFile: boolean): void => { + if (key === p || !key.startsWith(prefix)) return; const rest = key.slice(prefix.length); const first = rest.split('/')[0]; - if (first !== undefined && first.length > 0) children.add(first); + if (first === undefined || first.length === 0) return; + if (rest.includes('/')) addDir(first); + else if (isFile) addFile(first); + else addDir(first); }; - for (const f of fileMap.keys()) addChild(f); - for (const d of dirSet) addChild(d); - return [...children]; + for (const d of dirSet) visit(d, false); + for (const f of fileMap.keys()) visit(f, true); + return [...children.values()]; }, - glob: async () => [], mkdir: async (p, options) => { - const existOk = options?.existOk ?? true; - if ((dirSet.has(p) || fileMap.has(p)) && !existOk) { + const recursive = options?.recursive ?? false; + const exists = isDir(p) || fileMap.has(p); + if (recursive) { + // Add every ancestor up to (but not including) WORK_DIR, mirroring + // `fs.mkdir(..., { recursive: true })` which never throws EEXIST. + let current = p; + while (current !== WORK_DIR && current.length > WORK_DIR.length) { + dirSet.add(current); + const next = current.slice(0, current.lastIndexOf('/')); + if (next === current || next === '') break; + current = next; + } + dirSet.add(p); + return; + } + if (exists) { const err = new Error(`EEXIST: ${p}`) as NodeJS.ErrnoException; err.code = 'EEXIST'; throw err; } - const parents = options?.parents ?? true; - if (!parents) { - const parent = p.split('/').slice(0, -1).join('/'); - if (parent !== '' && !isDir(parent)) { - const err = new Error(`ENOENT: ${p}`) as NodeJS.ErrnoException; - err.code = 'ENOENT'; - throw err; - } + const parent = p.slice(0, p.lastIndexOf('/')); + if (parent !== '' && parent !== WORK_DIR && !isDir(parent)) { + const err = new Error(`ENOENT: ${p}`) as NodeJS.ErrnoException; + err.code = 'ENOENT'; + throw err; } dirSet.add(p); }, - withCwd: () => { - throw new Error('not implemented'); - }, + remove: async () => {}, }; } @@ -215,7 +240,7 @@ function makeSession( host = createScopedTestHost(); const session = host.child(LifecycleScope.Session, 's1', [ stubPair(ISessionWorkspaceContext, stubWorkspace()), - stubPair(ISessionAgentFileSystem, fakeFs(files)), + stubPair(IHostFileSystem, fakeFs(files)), stubPair(ISessionProcessRunner, fakeRunner(handler)), stubPair(ITelemetryService, telemetryStub(events)), stubPair(IGitService, git), diff --git a/packages/agent-core-v2/test/harness/agent.ts b/packages/agent-core-v2/test/harness/agent.ts index 544983a27..3c4823a2b 100644 --- a/packages/agent-core-v2/test/harness/agent.ts +++ b/packages/agent-core-v2/test/harness/agent.ts @@ -93,7 +93,7 @@ import { IAgentPermissionGate, IAgentPermissionModeService, IAgentPermissionRulesService, - ISessionAgentFileSystem, + IHostFileSystem, ISessionContext, ISessionProcessRunner, IAgentScopeContext, @@ -149,7 +149,7 @@ import { ISessionSwarmService } from '#/session/swarm'; import type { PathAccessOperation } from '#/session/workspaceContext'; import { recordAgentEvents, type RecordedEventEntry } from '../snapshot/events'; -import { createFakeAgentFs, createFakeProcessRunner } from '../tools/fixtures/fake-exec'; +import { createFakeHostFs, createFakeProcessRunner } from '../tools/fixtures/fake-exec'; import { createScriptedGenerate } from './scripted-generate'; import { DEFAULT_TEST_SYSTEM_PROMPT, @@ -397,7 +397,7 @@ type ExecContextOverride = { */ export interface ExecEnvOverride { readonly execContext?: ExecContextOverride; - readonly agentFs?: ISessionAgentFileSystem | Partial; + readonly hostFs?: IHostFileSystem | Partial; readonly processRunner?: ISessionProcessRunner | Partial; } @@ -414,8 +414,8 @@ export function execEnvServices(override: ExecEnvOverride = {}): TestAgentServic createExecContext(override.execContext.cwd ?? '/workspace', override.execContext.envLayers), ); } - if (override.agentFs !== undefined) { - reg.defineInstance(ISessionAgentFileSystem, resolveAgentFsOverride(override.agentFs)); + if (override.hostFs !== undefined) { + reg.defineInstance(IHostFileSystem, resolveHostFsOverride(override.hostFs)); } if (override.processRunner !== undefined) { reg.defineInstance( @@ -430,30 +430,28 @@ export function execEnvServices(override: ExecEnvOverride = {}): TestAgentServic }); } -function resolveAgentFsOverride( - input: ISessionAgentFileSystem | Partial, -): ISessionAgentFileSystem { +function resolveHostFsOverride(input: IHostFileSystem | Partial): IHostFileSystem { // A full impl (class instance or `Proxy` over one) exposes every core // method. A partial override typically covers only a few of them. If every // core method is a function, pass the input through unchanged; otherwise // treat it as a partial override and spread it over the fake defaults. - if (isFullAgentFs(input)) return input as ISessionAgentFileSystem; - return createFakeAgentFs(input as Partial); + if (isFullHostFs(input)) return input as IHostFileSystem; + return createFakeHostFs(input as Partial); } -function isFullAgentFs(input: unknown): boolean { +function isFullHostFs(input: unknown): boolean { if (typeof input !== 'object' || input === null) return false; - const keys: readonly (keyof ISessionAgentFileSystem)[] = [ + const keys: readonly (keyof IHostFileSystem)[] = [ 'readText', 'writeText', 'readBytes', - 'readLines', 'writeBytes', + 'readLines', + 'createExclusive', 'stat', 'readdir', - 'glob', 'mkdir', - 'withCwd', + 'remove', ]; return keys.every((k) => typeof (input as Record)[k] === 'function'); } @@ -990,9 +988,9 @@ export class AgentTestContext { reg.defineInstance(ISessionApprovalService, this.createApprovalService()); reg.defineInstance(ISessionQuestionService, this.createQuestionService()); reg.defineInstance(IExecContext, createExecContext(this.cwd)); - // Note: `ISessionAgentFileSystem` and `ISessionProcessRunner` are - // auto-registered by their service files and backed by `IExecContext`. - // Tests that need a fake override them via `execEnvServices`. + // Note: the os `IHostFileSystem` (App scope) and `ISessionProcessRunner` + // are auto-registered by their service files. Tests that need a fake + // filesystem override it via `execEnvServices({ hostFs })`. reg.defineDescriptor( ISessionWorkspaceContext, new SyncDescriptor(SessionWorkspaceContextService), diff --git a/packages/agent-core-v2/test/media/read-media.test.ts b/packages/agent-core-v2/test/media/read-media.test.ts index 5642ce8f9..27045ddab 100644 --- a/packages/agent-core-v2/test/media/read-media.test.ts +++ b/packages/agent-core-v2/test/media/read-media.test.ts @@ -1,14 +1,14 @@ /** * ReadMediaFileTool tests for the v2 output/capability contract. * - * Self-contained: builds minimal fake `ISessionAgentFileSystem` and `IKaos` inline - * so the tool can be exercised without the missing composition root. + * Self-contained: builds a minimal fake `IHostFileSystem` inline so the tool can + * be exercised without the missing composition root. */ import type { ContentPart, ModelCapability } from '#/app/llmProtocol/kosong'; import { describe, expect, it, vi } from 'vitest'; -import type { ISessionAgentFileSystem } from '#/session/agentFs'; +import type { IHostFileSystem } from '#/os/interface/hostFileSystem'; import type { IHostEnvironment } from '#/os/interface/hostEnvironment'; import { ReadMediaFileInputSchema, @@ -65,10 +65,9 @@ interface FakeFile { readonly size?: number; } -function createTestFs(files: Record): ISessionAgentFileSystem { +function createTestFs(files: Record): IHostFileSystem { const lookup = (path: string): FakeFile | undefined => files[path]; return { - cwd: '/workspace', readBytes: vi.fn(async (path: string, _n?: number) => lookup(path)?.data ?? Buffer.alloc(0)), stat: vi.fn(async (path: string) => { const file = lookup(path); @@ -78,7 +77,7 @@ function createTestFs(files: Record): ISessionAgentFileSystem size: file?.size ?? file?.data.length ?? 0, }; }), - } as unknown as ISessionAgentFileSystem; + } as unknown as IHostFileSystem; } function createTestEnv(): IHostEnvironment { diff --git a/packages/agent-core-v2/test/plan/injection.test.ts b/packages/agent-core-v2/test/plan/injection.test.ts index d806b2ef7..7896b0c32 100644 --- a/packages/agent-core-v2/test/plan/injection.test.ts +++ b/packages/agent-core-v2/test/plan/injection.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { createFakeAgentFs } from '../tools/fixtures/fake-exec'; +import { createFakeHostFs } from '../tools/fixtures/fake-exec'; import { IAgentContextInjectorService } from '#/agent/contextInjector'; import { IAgentContextMemoryService, type ContextMessage } from '#/agent/contextMemory'; import { IAgentPlanService } from '#/agent/plan'; @@ -62,7 +62,7 @@ describe('PlanModeService dynamic injection content', () => { beforeEach(() => { readText = async () => ''; ctx = createTestAgent(execEnvServices({ - agentFs: createFakeAgentFs({ + hostFs: createFakeHostFs({ mkdir: vi.fn().mockResolvedValue(undefined), readText: (path: string) => readText(path), writeText: vi.fn(async () => undefined), @@ -144,7 +144,7 @@ describe('PlanModeService dynamic injection cadence', () => { beforeEach(() => { ctx = createTestAgent(execEnvServices({ - agentFs: createFakeAgentFs({ + hostFs: createFakeHostFs({ mkdir: vi.fn().mockResolvedValue(undefined), readText: async () => '', writeText: vi.fn(async () => undefined), diff --git a/packages/agent-core-v2/test/plan/plan-tools-telemetry.test.ts b/packages/agent-core-v2/test/plan/plan-tools-telemetry.test.ts index a48a2ff68..bde73849a 100644 --- a/packages/agent-core-v2/test/plan/plan-tools-telemetry.test.ts +++ b/packages/agent-core-v2/test/plan/plan-tools-telemetry.test.ts @@ -12,7 +12,7 @@ import type { ITelemetryService } from '#/app/telemetry'; import { IAgentToolExecutorService } from '#/agent/toolExecutor'; import { executeTool } from '../tools/fixtures/execute-tool'; -import { createFakeAgentFs } from '../tools/fixtures/fake-exec'; +import { createFakeHostFs } from '../tools/fixtures/fake-exec'; import { createTestAgent, execEnvServices, @@ -218,7 +218,7 @@ describe('AgentPlanService EnterPlanMode telemetry', () => { records.splice(0); ctx = createTestAgent( execEnvServices({ - agentFs: createFakeAgentFs({ + hostFs: createFakeHostFs({ mkdir: vi.fn().mockResolvedValue(undefined), }), }), diff --git a/packages/agent-core-v2/test/plan/plan.test.ts b/packages/agent-core-v2/test/plan/plan.test.ts index e4bcda5b9..39d79b1b4 100644 --- a/packages/agent-core-v2/test/plan/plan.test.ts +++ b/packages/agent-core-v2/test/plan/plan.test.ts @@ -10,9 +10,9 @@ import { IAgentContextMemoryService } from '#/agent/contextMemory'; import { IAgentPlanService, type PlanData } from '#/agent/plan'; import { IAgentPermissionRulesService } from '#/agent/permissionRules'; import { IAgentProfileService } from '#/agent/profile'; -import type { ISessionAgentFileSystem } from '#/session/agentFs'; +import type { IHostFileSystem } from '#/os/interface/hostFileSystem'; import type { ISessionProcessRunner } from '#/session/process'; -import { createFakeAgentFs, createFakeProcessRunner } from '../tools/fixtures/fake-exec'; +import { createFakeHostFs, createFakeProcessRunner } from '../tools/fixtures/fake-exec'; import { createCommandRunner, createTestAgent, @@ -21,7 +21,7 @@ import { } from '../harness'; interface PlanFakes { - readonly fs: ISessionAgentFileSystem; + readonly fs: IHostFileSystem; readonly runner: ISessionProcessRunner; } @@ -30,8 +30,8 @@ interface PlanFakes { * readText no-op, runner throws). Individual tests override the specific * methods they need. */ -function createPlanFakes(overrides: Partial = {}): PlanFakes { - const fs = createFakeAgentFs({ +function createPlanFakes(overrides: Partial = {}): PlanFakes { + const fs = createFakeHostFs({ mkdir: vi.fn().mockResolvedValue(undefined), readText: vi.fn().mockResolvedValue(''), ...overrides, @@ -49,7 +49,7 @@ function createPlanCommandFakes(stdout: string): PlanFakes { function createPlanFileFakes( files = new Map(), - overrides: Partial = {}, + overrides: Partial = {}, ): { readonly files: Map; readonly readText: ReturnType; @@ -91,7 +91,7 @@ describe('Plan service', () => { tempDirs = []; ctx = createTestAgent( execEnvServices({ - agentFs: delegatingFs(), + hostFs: delegatingFs(), processRunner: delegatingRunner(), }), ); @@ -115,13 +115,13 @@ describe('Plan service', () => { * A fs whose methods delegate to whichever `activeFakes.fs` is set at call * time. Lets a test swap fakes mid-flight by reassigning `activeFakes`. */ - function delegatingFs(): ISessionAgentFileSystem { + function delegatingFs(): IHostFileSystem { return new Proxy(createPlanFakes().fs, { get(_target, prop, receiver) { const value = Reflect.get(activeFakes.fs, prop, receiver); return typeof value === 'function' ? value.bind(activeFakes.fs) : value; }, - }) as ISessionAgentFileSystem; + }) as IHostFileSystem; } function delegatingRunner(): ISessionProcessRunner { @@ -184,7 +184,7 @@ describe('Plan service', () => { const status = await expectActivePlan(); expect(status.path.startsWith(`${join(cwd, 'plan')}/`)).toBe(true); expect(status.path.endsWith('.md')).toBe(true); - expect(mkdir).toHaveBeenCalledWith(join(cwd, 'plan')); + expect(mkdir).toHaveBeenCalledWith(join(cwd, 'plan'), { recursive: true }); expect(writeText).not.toHaveBeenCalled(); expect(ctx.allEvents.some((event) => event.event === 'turn.started')).toBe(false); expect(ctx.llmCalls).toHaveLength(0); diff --git a/packages/agent-core-v2/test/profile/apply-profile.test.ts b/packages/agent-core-v2/test/profile/apply-profile.test.ts index 454811e75..d5c04dc83 100644 --- a/packages/agent-core-v2/test/profile/apply-profile.test.ts +++ b/packages/agent-core-v2/test/profile/apply-profile.test.ts @@ -4,7 +4,7 @@ import { join } from 'pathe'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { SessionAgentFileSystem } from '#/session/agentFs/agentFsService'; +import { HostFileSystem } from '#/os/backends/node-local/hostFsService'; import { createExecContext } from '#/session/execContext'; import { IAgentProfileService, type ResolvedAgentProfile } from '#/agent/profile'; @@ -38,12 +38,12 @@ describe('AgentProfileService.applyProfile', () => { // (empty temp dir) so a developer's real ~/.kimi-code / ~/.agents files // never leak into the assertions. const execCtx = createExecContext(workDir); - const fs = new SessionAgentFileSystem(execCtx); + const fs = new HostFileSystem(); ctx = createTestAgent( execEnvServices({ hostEnvironment: { homeDir }, execContext: execCtx, - agentFs: fs, + hostFs: fs, }), ); return { ctx, profile: ctx.get(IAgentProfileService) }; diff --git a/packages/agent-core-v2/test/profile/context.test.ts b/packages/agent-core-v2/test/profile/context.test.ts index af1b2ee14..0f2a7b813 100644 --- a/packages/agent-core-v2/test/profile/context.test.ts +++ b/packages/agent-core-v2/test/profile/context.test.ts @@ -4,21 +4,20 @@ import { join } from 'pathe'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { SessionAgentFileSystem } from '#/session/agentFs/agentFsService'; -import type { ISessionAgentFileSystem } from '#/session/agentFs'; -import { createExecContext } from '#/session/execContext'; +import { HostFileSystem } from '#/os/backends/node-local/hostFsService'; +import type { IHostFileSystem } from '#/os/interface/hostFileSystem'; import { loadAgentsMd, prepareSystemPromptContext } from '#/agent/profile'; /** - * Build a `SessionAgentFileSystem` rooted at `workDir` — the v2 profile - * context loaders take `{ fs, homeDir }` and read every AGENTS.md through the - * fs's `readText` / `readdir` / `stat`. + * Build an os-backed `IHostFileSystem`. The v2 profile context loaders take + * `{ fs, homeDir }` and read every AGENTS.md through the fs's `readText` / + * `readdir` / `stat` using absolute paths, so no cwd rooting is needed. */ -function createFs(workDir: string): ISessionAgentFileSystem { - return new SessionAgentFileSystem(createExecContext(workDir)); +function createFs(): IHostFileSystem { + return new HostFileSystem(); } -let fs: ISessionAgentFileSystem; +let fs: IHostFileSystem; let homeDir: string; let workDir: string; let extraDirs: string[]; @@ -27,7 +26,7 @@ beforeEach(async () => { homeDir = await mkdtemp(join(tmpdir(), 'kimi-agents-home-')); workDir = await mkdtemp(join(tmpdir(), 'kimi-agents-work-')); extraDirs = []; - fs = createFs(workDir); + fs = createFs(); }); afterEach(async () => { diff --git a/packages/agent-core-v2/test/session/session-warning.test.ts b/packages/agent-core-v2/test/session/session-warning.test.ts index 469095b5c..407019d95 100644 --- a/packages/agent-core-v2/test/session/session-warning.test.ts +++ b/packages/agent-core-v2/test/session/session-warning.test.ts @@ -14,7 +14,8 @@ import { type ScopedTestHost, createScopedTestHost, stubPair } from '#/_base/di/ import { IAgentLifecycleService } from '#/session/agentLifecycle'; import { IBootstrapService } from '#/app/bootstrap'; import { IHostEnvironment } from '#/os/interface/hostEnvironment'; -import { SessionAgentFileSystem, ISessionAgentFileSystem } from '#/session/agentFs'; +import { HostFileSystem } from '#/os/backends/node-local/hostFsService'; +import { IHostFileSystem } from '#/os/interface/hostFileSystem'; import { createExecContext, IExecContext } from '#/session/execContext'; import { IAgentProfileService } from '#/agent/profile'; import { ISessionWarningService, SessionWarningService } from '#/session/session'; @@ -68,10 +69,10 @@ function build(args: { stubPair(IHostEnvironment, hostEnvironment(args.homeDir)), ]); const ctx = createExecContext(args.workDir); - const fs: ISessionAgentFileSystem = new SessionAgentFileSystem(ctx); + const fs: IHostFileSystem = new HostFileSystem(); const session = host.child(LifecycleScope.Session, 's1', [ stubPair(IExecContext, ctx), - stubPair(ISessionAgentFileSystem, fs), + stubPair(IHostFileSystem, fs), stubPair(ISessionWorkspaceContext, workspaceStub(args.additionalDirs ?? [])), stubPair( IAgentLifecycleService, diff --git a/packages/agent-core-v2/test/tools/fixtures/fake-exec.ts b/packages/agent-core-v2/test/tools/fixtures/fake-exec.ts index c719ee47d..646eb215b 100644 --- a/packages/agent-core-v2/test/tools/fixtures/fake-exec.ts +++ b/packages/agent-core-v2/test/tools/fixtures/fake-exec.ts @@ -1,4 +1,4 @@ -import type { ISessionAgentFileSystem } from '#/session/agentFs'; +import type { IHostFileSystem } from '#/os/interface/hostFileSystem'; import type { ISessionProcessRunner } from '#/session/process'; function notImplemented(name: string): never { @@ -15,23 +15,19 @@ export function createFakeProcessRunner( }; } -export function createFakeAgentFs( - overrides: Partial = {}, -): ISessionAgentFileSystem { - const cwd = overrides.cwd ?? '/workspace'; - const fs: ISessionAgentFileSystem = { +export function createFakeHostFs(overrides: Partial = {}): IHostFileSystem { + const fs: IHostFileSystem = { _serviceBrand: undefined, - cwd, - readText: () => notImplemented('FakeAgentFs.readText'), - writeText: () => notImplemented('FakeAgentFs.writeText'), - readBytes: () => notImplemented('FakeAgentFs.readBytes'), - readLines: () => notImplemented('FakeAgentFs.readLines'), - writeBytes: () => notImplemented('FakeAgentFs.writeBytes'), - stat: () => notImplemented('FakeAgentFs.stat'), - readdir: () => notImplemented('FakeAgentFs.readdir'), - glob: () => notImplemented('FakeAgentFs.glob'), - mkdir: () => notImplemented('FakeAgentFs.mkdir'), - withCwd: (nextCwd) => createFakeAgentFs({ ...overrides, cwd: nextCwd }), + readText: () => notImplemented('FakeHostFs.readText'), + writeText: () => notImplemented('FakeHostFs.writeText'), + readBytes: () => notImplemented('FakeHostFs.readBytes'), + writeBytes: () => notImplemented('FakeHostFs.writeBytes'), + readLines: () => notImplemented('FakeHostFs.readLines'), + createExclusive: () => notImplemented('FakeHostFs.createExclusive'), + stat: () => notImplemented('FakeHostFs.stat'), + readdir: () => notImplemented('FakeHostFs.readdir'), + mkdir: () => notImplemented('FakeHostFs.mkdir'), + remove: () => notImplemented('FakeHostFs.remove'), }; return { ...fs, ...overrides }; } diff --git a/packages/agent-core-v2/test/turn/turn.test.ts b/packages/agent-core-v2/test/turn/turn.test.ts index 2118cfd53..d2c3e55d8 100644 --- a/packages/agent-core-v2/test/turn/turn.test.ts +++ b/packages/agent-core-v2/test/turn/turn.test.ts @@ -17,7 +17,7 @@ import { import { describe, expect, it, vi } from 'vitest'; import { abortError, abortable } from '#/_base/utils/abort'; -import { ISessionAgentFileSystem } from '#/session/agentFs'; +import { IHostFileSystem } from '#/os/interface/hostFileSystem'; import type { ContextMessage } from '#/agent/contextMemory'; import { IHostEnvironment } from '#/os/interface/hostEnvironment'; import { IOAuthService } from '#/app/auth'; @@ -39,7 +39,7 @@ import type { SessionSwarmTask as QueuedSubagentTask, } from '#/session/swarm'; import { recordingTelemetry, type TelemetryRecord } from '../telemetry/stubs'; -import { createFakeAgentFs, createFakeProcessRunner } from '../tools/fixtures/fake-exec'; +import { createFakeHostFs, createFakeProcessRunner } from '../tools/fixtures/fake-exec'; import { configServices, appServices, @@ -1747,7 +1747,7 @@ describe('Agent turn flow', () => { throw new APIStatusError(401, 'Unauthorized', 'req-upload-401'); }), } as unknown as ChatProvider; - const ctx = testAgent(oauthOptions.services, execEnvServices({ agentFs: createVideoAgentFs() }), { + const ctx = testAgent(oauthOptions.services, execEnvServices({ hostFs: createVideoHostFs() }), { initialConfig: oauthOptions.initialConfig, autoConfigure: false, }); @@ -1767,7 +1767,7 @@ describe('Agent turn flow', () => { return uploadVideo.call(provider, input, { auth }); }); const registration = registerMediaTools(ctx.get(IAgentToolRegistryService), { - fs: ctx.get(ISessionAgentFileSystem), + fs: ctx.get(IHostFileSystem), env: ctx.get(IHostEnvironment), workspace: { workspaceDir: '/workspace', additionalDirs: [] }, capabilities: mediaCapabilities(), @@ -2090,8 +2090,8 @@ function createExecRunner(output: string): { return { runner: createFakeProcessRunner({ exec }), exec }; } -function createVideoAgentFs(): ISessionAgentFileSystem { - return createFakeAgentFs({ +function createVideoHostFs(): IHostFileSystem { + return createFakeHostFs({ stat: vi.fn(async () => ({ isFile: true, isDirectory: false, diff --git a/packages/agent-core-v2/test/wireRecord/resume.test.ts b/packages/agent-core-v2/test/wireRecord/resume.test.ts index f389252da..7a4eab99c 100644 --- a/packages/agent-core-v2/test/wireRecord/resume.test.ts +++ b/packages/agent-core-v2/test/wireRecord/resume.test.ts @@ -18,7 +18,7 @@ import { createAgentTaskPersistence, type TaskServiceTestManager, } from '../task/stubs'; -import { createFakeAgentFs, createFakeProcessRunner } from '../tools/fixtures/fake-exec'; +import { createFakeHostFs, createFakeProcessRunner } from '../tools/fixtures/fake-exec'; import { DEFAULT_TEST_SYSTEM_PROMPT, InMemoryWireRecordPersistence, @@ -66,7 +66,7 @@ describe('Agent resume', () => { const execWithEnv = vi.fn().mockRejectedValue(new Error('Bash should not execute on resume')); const ctx = testAgent( execEnvServices({ - agentFs: createFakeAgentFs({ readText: vi.fn().mockResolvedValue('') }), + hostFs: createFakeHostFs({ readText: vi.fn().mockResolvedValue('') }), processRunner: createFakeProcessRunner({ exec: execWithEnv }), }), { autoConfigure: false, persistence }, diff --git a/packages/server-e2e/src/v2/resources/manifest.ts b/packages/server-e2e/src/v2/resources/manifest.ts index 489efc80c..6b8649598 100644 --- a/packages/server-e2e/src/v2/resources/manifest.ts +++ b/packages/server-e2e/src/v2/resources/manifest.ts @@ -70,7 +70,6 @@ export const SESSION = { addAdditionalDir: RW, removeAdditionalDir: RW, }, - agentFs: { readText: RO, writeText: RW, stat: RO, readdir: RO, mkdir: RW }, fs: { search: RO, grep: RO, gitStatus: RO, diff: RO }, } as const satisfies Manifest; diff --git a/packages/server-e2e/src/v2/resources/session.ts b/packages/server-e2e/src/v2/resources/session.ts index 1bb24a734..46cdad538 100644 --- a/packages/server-e2e/src/v2/resources/session.ts +++ b/packages/server-e2e/src/v2/resources/session.ts @@ -4,7 +4,7 @@ * The `session` resource (read/update/setTitle/setArchived/status/isIdle/ * archive) is flattened onto the {@link SessionScope} handle itself, since it * is the primary thing you do with a session; every other resource - * (`approvals`, `questions`, `interactions`, `workspace`, `agentFs`, `fs`) is + * (`approvals`, `questions`, `interactions`, `workspace`, `fs`) is * a sub-namespace. `agent(agentId)` enters the agent scope, and * `service(resource)` is the escape hatch for actions not in the manifest. */ @@ -30,7 +30,6 @@ export type ApprovalsResource = ResourceShape; export type QuestionsResource = ResourceShape; export type InteractionsResource = ResourceShape; export type SessionWorkspaceResource = ResourceShape; -export type AgentFsResource = ResourceShape; export type SessionFsResource = ResourceShape; /** Session scope handle — obtained via `client.session(sessionId)`. */ @@ -39,7 +38,6 @@ export class SessionScope { readonly questions: QuestionsResource; readonly interactions: InteractionsResource; readonly workspace: SessionWorkspaceResource; - readonly agentFs: AgentFsResource; readonly fs: SessionFsResource; private readonly sessionResource: SessionResource; @@ -60,7 +58,6 @@ export class SessionScope { this.questions = makeResource(rpc, 'session', params, 'questions', SESSION.questions); this.interactions = makeResource(rpc, 'session', params, 'interactions', SESSION.interactions); this.workspace = makeResource(rpc, 'session', params, 'workspace', SESSION.workspace); - this.agentFs = makeResource(rpc, 'session', params, 'agentFs', SESSION.agentFs); this.fs = makeResource(rpc, 'session', params, 'fs', SESSION.fs); } diff --git a/packages/server-v2/src/transport/actionMap.ts b/packages/server-v2/src/transport/actionMap.ts index cca906042..6120af5a3 100644 --- a/packages/server-v2/src/transport/actionMap.ts +++ b/packages/server-v2/src/transport/actionMap.ts @@ -22,7 +22,6 @@ */ import { - ISessionAgentFileSystem, IAgentRPCService, ISessionApprovalService, IAuthSummaryService, @@ -156,12 +155,6 @@ export const actionMap: Record> = { 'workspace:addAdditionalDir': { service: ISessionWorkspaceContext, method: 'addAdditionalDir' }, 'workspace:removeAdditionalDir': { service: ISessionWorkspaceContext, method: 'removeAdditionalDir' }, - 'agentFs:readText': { service: ISessionAgentFileSystem, method: 'readText', readonly: true }, - 'agentFs:writeText': { service: ISessionAgentFileSystem, method: 'writeText' }, - 'agentFs:stat': { service: ISessionAgentFileSystem, method: 'stat', readonly: true }, - 'agentFs:readdir': { service: ISessionAgentFileSystem, method: 'readdir', readonly: true }, - 'agentFs:mkdir': { service: ISessionAgentFileSystem, method: 'mkdir' }, - 'fs:search': { service: ISessionFsService, method: 'search', readonly: true }, 'fs:grep': { service: ISessionFsService, method: 'grep', readonly: true }, 'fs:gitStatus': { service: ISessionFsService, method: 'gitStatus', readonly: true },