Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/core/src/tool/read-filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export class PathKindError extends Schema.TaggedErrorClass<PathKindError>()("Rea
}

export type InspectError = FSUtil.Error | PathKindError
export type ListError = FSUtil.Error | OffsetOutOfRangeError
export type ReadError =
| FSUtil.Error
| BinaryFileError
Expand Down Expand Up @@ -97,7 +98,7 @@ export interface Interface {
resource: string,
page?: PageInput,
) => Effect.Effect<FileSystem.Content | TextPage, ReadError>
readonly list: (path: AbsolutePath, page?: PageInput) => Effect.Effect<ListPage, FSUtil.Error>
readonly list: (path: AbsolutePath, page?: PageInput) => Effect.Effect<ListPage, ListError>
}

export class Service extends Context.Service<Service, Interface>()("@opencode/ReadToolFileSystem") {}
Expand Down Expand Up @@ -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 } : {}) })
})
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/tool/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions packages/core/test/tool-read-filesystem.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions packages/core/test/tool-read.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading