feat(core): bound v2 tool output (#30999)

This commit is contained in:
Kit Langton
2026-06-05 14:35:19 -04:00
committed by GitHub
parent 760d523847
commit a9094fd059
31 changed files with 390 additions and 555 deletions
+4 -15
View File
@@ -3,9 +3,7 @@ export * as ReadTool from "./read"
import { Tool, ToolFailure } from "@opencode-ai/llm"
import { Cause, Effect, Layer, Schema } from "effect"
import { FileSystem } from "../filesystem"
import { NonNegativeInt, PositiveInt } from "../schema"
import { PermissionV2 } from "../permission"
import { ToolOutputStore } from "../tool-output-store"
import { ToolRegistry } from "./registry"
export const name = "read"
@@ -18,17 +16,12 @@ const LocationInput = Schema.Struct({
description: "The maximum number of directory entries or text lines to read",
}),
})
const ResourceInput = Schema.Struct({
resource: Schema.String,
offset: NonNegativeInt.pipe(Schema.optional),
limit: PositiveInt.check(Schema.isLessThanOrEqualTo(ToolOutputStore.MAX_READ_BYTES)).pipe(Schema.optional),
})
const Input = Schema.Union([LocationInput, ResourceInput])
const Success = Schema.Union([FileSystem.Content, FileSystem.TextPage, FileSystem.ListPage, ToolOutputStore.Page])
const Input = LocationInput
const Success = Schema.Union([FileSystem.Content, FileSystem.TextPage, FileSystem.ListPage])
const definition = Tool.make({
description:
"Read a text or binary file, page through a large UTF-8 text file by line offset, list a directory page relative to the current location, or page through a managed tool-output resource by opaque URI.",
"Read a text or binary file, page through a large UTF-8 text file by line offset, or list a directory page relative to the current location. Absolute paths are accepted only for managed tool-output files.",
parameters: Input,
success: Success,
})
@@ -37,7 +30,6 @@ export const layer = Layer.effectDiscard(
Effect.gen(function* () {
const registry = yield* ToolRegistry.Service
const filesystem = yield* FileSystem.Service
const resources = yield* ToolOutputStore.Service
yield* registry.contribute((editor) =>
editor.set(name, {
@@ -45,8 +37,6 @@ export const layer = Layer.effectDiscard(
execute: ({ parameters, sessionID, assertPermission }) => {
const input = parameters
return Effect.gen(function* () {
if ("resource" in input)
return yield* resources.read({ sessionID, uri: input.resource, offset: input.offset, limit: input.limit })
const resolved = yield* filesystem.resolveReadPath(input)
if (resolved.type === "directory") {
const { offset, limit } = input
@@ -81,7 +71,7 @@ export const layer = Layer.effectDiscard(
Effect.catchCause((cause) =>
Effect.fail(
new ToolFailure({
message: `Unable to read ${"resource" in input ? input.resource : input.path}`,
message: `Unable to read ${input.path}`,
error: Cause.squash(cause),
}),
),
@@ -96,5 +86,4 @@ export const locationLayer = layer.pipe(
Layer.provideMerge(ToolRegistry.defaultLayer),
Layer.provideMerge(FileSystem.locationLayer),
Layer.provideMerge(PermissionV2.locationLayer),
Layer.provideMerge(ToolOutputStore.defaultLayer),
)