refactor(core): unify v2 tool architecture (#31168)
This commit is contained in:
parent
effd27b239
commit
660a00d317
46 changed files with 2297 additions and 2041 deletions
|
|
@ -4,19 +4,16 @@ import * as TestClock from "effect/testing/TestClock"
|
|||
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
|
||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
||||
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||
import { WebFetchTool } from "@opencode-ai/core/tool/webfetch"
|
||||
import { testEffect } from "./lib/effect"
|
||||
import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool"
|
||||
|
||||
const sessionID = SessionV2.ID.make("ses_webfetch_test")
|
||||
const requests: Array<{ readonly url: string; readonly headers: Record<string, string> }> = []
|
||||
const assertions: PermissionV2.AssertInput[] = []
|
||||
const truncations: ToolOutputStore.TruncateInput[] = []
|
||||
let respond = (_request: HttpClientRequest.HttpClientRequest) =>
|
||||
Effect.succeed(new Response("hello", { headers: { "content-type": "text/plain" } }))
|
||||
let truncate = (input: ToolOutputStore.TruncateInput): Effect.Effect<ToolOutputStore.TruncateResult> =>
|
||||
Effect.succeed({ content: input.content, truncated: false })
|
||||
|
||||
const http = Layer.succeed(
|
||||
HttpClient.HttpClient,
|
||||
|
|
@ -38,36 +35,25 @@ const permission = Layer.succeed(
|
|||
list: () => Effect.die("unused"),
|
||||
}),
|
||||
)
|
||||
const resources = Layer.succeed(
|
||||
ToolOutputStore.Service,
|
||||
ToolOutputStore.Service.of({
|
||||
limits: () => Effect.die("unused"),
|
||||
write: () => Effect.die("unused"),
|
||||
truncate: (input) => Effect.sync(() => truncations.push(input)).pipe(Effect.andThen(truncate(input))),
|
||||
bound: (input) => Effect.succeed({ output: input.output, outputPaths: [] }),
|
||||
cleanup: () => Effect.die("unused"),
|
||||
}),
|
||||
)
|
||||
const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
|
||||
const webfetch = WebFetchTool.layer.pipe(Layer.provide(registry), Layer.provide(http), Layer.provide(resources))
|
||||
const it = testEffect(Layer.mergeAll(registry, permission, http, resources, webfetch))
|
||||
const webfetch = WebFetchTool.layer.pipe(Layer.provide(registry), Layer.provide(permission), Layer.provide(http))
|
||||
const it = testEffect(Layer.mergeAll(registry, permission, http, webfetch))
|
||||
const fetchWebfetch = WebFetchTool.layer.pipe(
|
||||
Layer.provide(registry),
|
||||
Layer.provide(permission),
|
||||
Layer.provide(FetchHttpClient.layer),
|
||||
Layer.provide(resources),
|
||||
)
|
||||
const live = testEffect(Layer.mergeAll(registry, permission, FetchHttpClient.layer, resources, fetchWebfetch))
|
||||
const live = testEffect(Layer.mergeAll(registry, permission, FetchHttpClient.layer, fetchWebfetch))
|
||||
|
||||
const reset = () => {
|
||||
requests.length = 0
|
||||
assertions.length = 0
|
||||
truncations.length = 0
|
||||
respond = () => Effect.succeed(new Response("hello", { headers: { "content-type": "text/plain" } }))
|
||||
truncate = (input) => Effect.succeed({ content: input.content, truncated: false })
|
||||
}
|
||||
|
||||
const call = (input: typeof WebFetchTool.Parameters.Type, id = "call-webfetch") => ({
|
||||
sessionID,
|
||||
...toolIdentity,
|
||||
call: { type: "tool-call" as const, id, name: "webfetch", input },
|
||||
})
|
||||
|
||||
|
|
@ -93,15 +79,15 @@ describe("WebFetchTool contribution", () => {
|
|||
const registry = yield* ToolRegistry.Service
|
||||
const url = "http://example.com/public"
|
||||
|
||||
expect((yield* registry.definitions()).map((tool) => tool.name)).toEqual(["webfetch"])
|
||||
expect(yield* registry.settle(call({ url, format: "text", timeout: 4 }))).toEqual({
|
||||
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["webfetch"])
|
||||
expect(yield* settleTool(registry, call({ url, format: "text", timeout: 4 }))).toEqual({
|
||||
result: { type: "text", value: "hello" },
|
||||
output: {
|
||||
structured: { url, contentType: "text/plain", format: "text", output: "hello", truncated: false },
|
||||
structured: { url, contentType: "text/plain", format: "text", output: "hello" },
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
},
|
||||
})
|
||||
expect(assertions).toEqual([
|
||||
expect(assertions).toMatchObject([
|
||||
{ sessionID, action: "webfetch", resources: [url], save: ["*"], metadata: { url, format: "text", timeout: 4 } },
|
||||
])
|
||||
expect(requests).toMatchObject([{ url, headers: { accept: expect.stringContaining("text/plain;q=1.0") } }])
|
||||
|
|
@ -114,11 +100,11 @@ describe("WebFetchTool contribution", () => {
|
|||
const registry = yield* ToolRegistry.Service
|
||||
const url = "http://localhost/private"
|
||||
|
||||
expect(yield* registry.execute(call({ url, format: "text" }))).toEqual({
|
||||
expect(yield* executeTool(registry, call({ url, format: "text" }))).toEqual({
|
||||
type: "text",
|
||||
value: "hello",
|
||||
})
|
||||
expect(assertions).toEqual([
|
||||
expect(assertions).toMatchObject([
|
||||
{ sessionID, action: "webfetch", resources: [url], save: ["*"], metadata: { url, format: "text" } },
|
||||
])
|
||||
expect(requests.map((request) => request.url)).toEqual([url])
|
||||
|
|
@ -142,8 +128,11 @@ describe("WebFetchTool contribution", () => {
|
|||
const registry = yield* ToolRegistry.Service
|
||||
const url = new URL("/redirect", server.url).toString()
|
||||
|
||||
expect(yield* registry.execute(call({ url, format: "text" }))).toEqual({ type: "text", value: "redirected" })
|
||||
expect(assertions).toEqual([
|
||||
expect(yield* executeTool(registry, call({ url, format: "text" }))).toEqual({
|
||||
type: "text",
|
||||
value: "redirected",
|
||||
})
|
||||
expect(assertions).toMatchObject([
|
||||
{ sessionID, action: "webfetch", resources: [url], save: ["*"], metadata: { url, format: "text" } },
|
||||
])
|
||||
}),
|
||||
|
|
@ -156,7 +145,7 @@ describe("WebFetchTool contribution", () => {
|
|||
reset()
|
||||
const registry = yield* ToolRegistry.Service
|
||||
|
||||
expect(yield* registry.execute(call({ url: "file:///etc/passwd", format: "text" }))).toEqual({
|
||||
expect(yield* executeTool(registry, call({ url: "file:///etc/passwd", format: "text" }))).toEqual({
|
||||
type: "error",
|
||||
value: "Unable to fetch file:///etc/passwd",
|
||||
})
|
||||
|
|
@ -176,41 +165,17 @@ describe("WebFetchTool contribution", () => {
|
|||
)
|
||||
const registry = yield* ToolRegistry.Service
|
||||
|
||||
expect(yield* registry.execute(call({ url: "https://1.1.1.1", format: "markdown" }))).toEqual({
|
||||
expect(yield* executeTool(registry, call({ url: "https://1.1.1.1", format: "markdown" }))).toEqual({
|
||||
type: "text",
|
||||
value: "# Hello\n\nworld",
|
||||
})
|
||||
expect(yield* registry.execute(call({ url: "https://1.1.1.1", format: "text" }))).toEqual({
|
||||
expect(yield* executeTool(registry, call({ url: "https://1.1.1.1", format: "text" }))).toEqual({
|
||||
type: "text",
|
||||
value: "Helloworld",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("exposes managed overflow through a path", () =>
|
||||
Effect.gen(function* () {
|
||||
reset()
|
||||
truncate = (input) =>
|
||||
Effect.succeed({
|
||||
content: "HEAD\n\n... output truncated; full content saved to /tmp/tool-output/tool_opaque ...\n\nTAIL",
|
||||
truncated: true,
|
||||
outputPath: "/tmp/tool-output/tool_opaque",
|
||||
})
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const settled = yield* registry.settle(call({ url: "https://1.1.1.1", format: "html" }, "call-overflow"))
|
||||
|
||||
expect(settled.result).toMatchObject({
|
||||
type: "text",
|
||||
value: expect.stringContaining("/tmp/tool-output/tool_opaque"),
|
||||
})
|
||||
expect(settled.output?.structured).toMatchObject({
|
||||
truncated: true,
|
||||
outputPath: "/tmp/tool-output/tool_opaque",
|
||||
})
|
||||
expect(truncations).toEqual([{ sessionID, toolCallID: "call-overflow", content: "hello", mime: "text/html" }])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects declared and streamed oversized bodies", () =>
|
||||
Effect.gen(function* () {
|
||||
reset()
|
||||
|
|
@ -221,7 +186,7 @@ describe("WebFetchTool contribution", () => {
|
|||
headers: { "content-type": "text/plain", "content-length": String(WebFetchTool.MAX_RESPONSE_BYTES + 1) },
|
||||
}),
|
||||
)
|
||||
expect(yield* registry.execute(call({ url: "https://1.1.1.1/declared", format: "text" }))).toEqual({
|
||||
expect(yield* executeTool(registry, call({ url: "https://1.1.1.1/declared", format: "text" }))).toEqual({
|
||||
type: "error",
|
||||
value: "Unable to fetch https://1.1.1.1/declared",
|
||||
})
|
||||
|
|
@ -230,7 +195,7 @@ describe("WebFetchTool contribution", () => {
|
|||
Effect.succeed(
|
||||
new Response("x".repeat(WebFetchTool.MAX_RESPONSE_BYTES + 1), { headers: { "content-type": "text/plain" } }),
|
||||
)
|
||||
expect(yield* registry.execute(call({ url: "https://1.1.1.1/streamed", format: "text" }))).toEqual({
|
||||
expect(yield* executeTool(registry, call({ url: "https://1.1.1.1/streamed", format: "text" }))).toEqual({
|
||||
type: "error",
|
||||
value: "Unable to fetch https://1.1.1.1/streamed",
|
||||
})
|
||||
|
|
@ -242,17 +207,16 @@ describe("WebFetchTool contribution", () => {
|
|||
reset()
|
||||
const registry = yield* ToolRegistry.Service
|
||||
respond = () => Effect.succeed(new Response("png", { headers: { "content-type": "image/png" } }))
|
||||
expect(yield* registry.execute(call({ url: "https://1.1.1.1/image", format: "html" }))).toEqual({
|
||||
expect(yield* executeTool(registry, call({ url: "https://1.1.1.1/image", format: "html" }))).toEqual({
|
||||
type: "error",
|
||||
value: "Unable to fetch https://1.1.1.1/image",
|
||||
})
|
||||
|
||||
respond = () => Effect.succeed(new Response("pdf", { headers: { "content-type": "application/pdf" } }))
|
||||
expect(yield* registry.execute(call({ url: "https://1.1.1.1/file", format: "html" }))).toEqual({
|
||||
expect(yield* executeTool(registry, call({ url: "https://1.1.1.1/file", format: "html" }))).toEqual({
|
||||
type: "error",
|
||||
value: "Unable to fetch https://1.1.1.1/file",
|
||||
})
|
||||
expect(truncations).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -268,7 +232,7 @@ describe("WebFetchTool contribution", () => {
|
|||
)
|
||||
const registry = yield* ToolRegistry.Service
|
||||
|
||||
expect(yield* registry.execute(call({ url: "https://1.1.1.1", format: "text" }))).toEqual({
|
||||
expect(yield* executeTool(registry, call({ url: "https://1.1.1.1", format: "text" }))).toEqual({
|
||||
type: "text",
|
||||
value: "ok",
|
||||
})
|
||||
|
|
@ -283,9 +247,10 @@ describe("WebFetchTool contribution", () => {
|
|||
reset()
|
||||
respond = () => Effect.never
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const fiber = yield* registry
|
||||
.execute(call({ url: "https://1.1.1.1/slow", format: "text", timeout: 1 }))
|
||||
.pipe(Effect.forkChild)
|
||||
const fiber = yield* executeTool(
|
||||
registry,
|
||||
call({ url: "https://1.1.1.1/slow", format: "text", timeout: 1 }),
|
||||
).pipe(Effect.forkChild)
|
||||
yield* TestClock.adjust(Duration.seconds(1))
|
||||
|
||||
expect(yield* Fiber.join(fiber)).toEqual({ type: "error", value: "Unable to fetch https://1.1.1.1/slow" })
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue