diff --git a/packages/core/src/tool/read-filesystem.ts b/packages/core/src/tool/read-filesystem.ts index e325a83edd2c..c499dcc140ad 100644 --- a/packages/core/src/tool/read-filesystem.ts +++ b/packages/core/src/tool/read-filesystem.ts @@ -61,6 +61,7 @@ export class PathKindError extends Schema.TaggedErrorClass()("Rea } export type InspectError = FSUtil.Error | PathKindError +export type ListError = FSUtil.Error | OffsetOutOfRangeError export type ReadError = | FSUtil.Error | BinaryFileError @@ -97,7 +98,7 @@ export interface Interface { resource: string, page?: PageInput, ) => Effect.Effect - readonly list: (path: AbsolutePath, page?: PageInput) => Effect.Effect + readonly list: (path: AbsolutePath, page?: PageInput) => Effect.Effect } export class Service extends Context.Service()("@opencode/ReadToolFileSystem") {} @@ -347,6 +348,7 @@ export const list = Effect.fn("ReadTool.list")(function* (fs: FSUtil.Interface, .filter((item): item is FileSystem.Entry => item !== undefined) .sort((a, b) => (a.type === b.type ? a.path.localeCompare(b.path) : a.type === "directory" ? -1 : 1)) const selected = visible.slice(offset - 1, offset - 1 + limit) + if (selected.length === 0 && offset !== 1) return yield* Effect.fail(new OffsetOutOfRangeError({ offset })) const truncated = offset - 1 + selected.length < visible.length return new ListPage({ entries: selected, truncated, ...(truncated ? { next: offset + selected.length } : {}) }) }) diff --git a/packages/core/src/tool/read.ts b/packages/core/src/tool/read.ts index 6961a8609118..a2349bd15e9c 100644 --- a/packages/core/src/tool/read.ts +++ b/packages/core/src/tool/read.ts @@ -96,6 +96,7 @@ const layer = Layer.effectDiscard( const message = error instanceof ReadToolFileSystem.BinaryFileError || error instanceof ReadToolFileSystem.MediaIngestLimitError || + error instanceof ReadToolFileSystem.OffsetOutOfRangeError || error instanceof Image.DecodeError || error instanceof Image.SizeError ? error.message diff --git a/packages/core/test/tool-read-filesystem.test.ts b/packages/core/test/tool-read-filesystem.test.ts index e9970fb975da..f9914d413e3c 100644 --- a/packages/core/test/tool-read-filesystem.test.ts +++ b/packages/core/test/tool-read-filesystem.test.ts @@ -82,6 +82,29 @@ describe("ReadToolFileSystem", () => { }), ) + it.effect("reports out-of-range directory pagination as a typed error", () => + Effect.gen(function* () { + const { fs, files, directory } = yield* fixture + yield* files.writeFileString(path.join(directory, "a.txt"), "a") + yield* files.writeFileString(path.join(directory, "b.txt"), "b") + + const error = yield* ReadToolFileSystem.list(fs, directory, { offset: 100 }).pipe(Effect.flip) + + expect(error).toBeInstanceOf(ReadToolFileSystem.OffsetOutOfRangeError) + expect(error.message).toBe("Offset 100 is out of range") + }), + ) + + it.effect("returns an empty directory page at offset 1", () => + Effect.gen(function* () { + const { fs, directory } = yield* fixture + + const result = yield* ReadToolFileSystem.list(fs, directory, { offset: 1 }) + + expect(result).toMatchObject({ entries: [], truncated: false }) + }), + ) + it.effect("stops reading after the requested page is complete", () => Effect.gen(function* () { const { fs, files, directory } = yield* fixture diff --git a/packages/core/test/tool-read.test.ts b/packages/core/test/tool-read.test.ts index 29b32bd03dcb..90e73b453bb0 100644 --- a/packages/core/test/tool-read.test.ts +++ b/packages/core/test/tool-read.test.ts @@ -511,6 +511,26 @@ describe("ReadTool", () => { }), ) + it.effect("returns out-of-range offsets to the model", () => + Effect.gen(function* () { + readFailure = new ReadToolFileSystem.OffsetOutOfRangeError({ offset: 100 }) + const registry = yield* ToolRegistry.Service + + expect( + yield* executeTool(registry, { + sessionID, + ...toolIdentity, + call: { + type: "tool-call", + id: "call-offset", + name: "read", + input: { path: "short.txt", offset: 100 }, + }, + }), + ).toEqual({ type: "error", value: "Offset 100 is out of range" }) + }), + ) + it.effect("preserves unexpected filesystem defects", () => Effect.gen(function* () { resolveFailure = new Error("unexpected")