fix(core): normalize tool images once (#41097)

This commit is contained in:
Kit Langton
2026-08-07 13:02:13 -04:00
committed by GitHub
parent e5ef00b8b8
commit b4f769f695
2 changed files with 44 additions and 6 deletions
+1 -2
View File
@@ -118,13 +118,12 @@ const layer = Layer.effect(
yield* hooks.trigger("tool", "execute.after", afterEvent)
return yield* afterEvent.error
}
const content = yield* normalizeImages(execution.value.content)
const afterEvent: PluginHooks.Domains["tool"]["execute.after"] = {
...base,
status: "completed",
result: {
...(execution.value.output === undefined ? {} : { output: execution.value.output }),
content: content.length > 0 ? content : execution.value.content,
content: execution.value.content,
...(execution.value.metadata === undefined ? {} : { metadata: execution.value.metadata }),
},
}
@@ -3,10 +3,12 @@ import { Agent } from "@opencode-ai/core/agent"
import type { Permission } from "@opencode-ai/core/permission"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { Image } from "@opencode-ai/core/image"
import { PluginHooks } from "@opencode-ai/core/plugin/hooks"
import { Session } from "@opencode-ai/core/session"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { Tool } from "@opencode-ai/core/tool"
import type { Info } from "@opencode-ai/schema/tool"
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { executeTool, toolDefinitions } from "./lib/tool"
import { Cause, Deferred, Effect, Exit, Fiber, Layer, Option, Schema, SchemaGetter, SchemaIssue, Scope } from "effect"
import { testEffect } from "./lib/effect"
@@ -26,10 +28,14 @@ const imageStore = Layer.mock(Image.Service, {
maxBytes: 5,
}),
)
return Effect.succeed({ ...content, content: "bm9ybWFsaXplZA==", mime: "image/jpeg" })
return Effect.succeed({
...content,
content: Buffer.from(`${Buffer.from(content.content, "base64").toString()} normalized`).toString("base64"),
mime: "image/jpeg",
})
},
})
const registryLayer = AppNodeBuilder.build(Tool.node, [[Image.node, imageStore]])
const registryLayer = AppNodeBuilder.build(LayerNode.group([Tool.node, PluginHooks.node]), [[Image.node, imageStore]])
const it = testEffect(registryLayer)
const identity = {
agent: Agent.ID.make("build"),
@@ -344,7 +350,7 @@ describe("Tool", () => {
}),
)
it.effect("normalizes image tool output at execution and drops unresizable images", () =>
it.effect("normalizes image tool output once and drops unresizable images", () =>
Effect.gen(function* () {
const service = yield* Tool.Service
yield* transform(service,
@@ -376,7 +382,12 @@ describe("Tool", () => {
const execution = yield* executeTool(service, call("snapshot"))
expect(execution.content).toEqual([
{ type: "file", uri: "data:image/jpeg;base64,bm9ybWFsaXplZA==", mime: "image/jpeg", name: "frame.png" },
{
type: "file",
uri: "data:image/jpeg;base64,aW1hZ2Ugbm9ybWFsaXplZA==",
mime: "image/jpeg",
name: "frame.png",
},
{ type: "text", text: "snapshot" },
{ type: "text", text: "[1 image omitted: could not be decoded.]" },
{ type: "text", text: "[1 image omitted: could not be resized below the image size limit.]" },
@@ -384,6 +395,34 @@ describe("Tool", () => {
}),
)
it.effect("normalizes image content added by an after hook", () =>
Effect.gen(function* () {
const service = yield* Tool.Service
const hooks = yield* PluginHooks.Service
yield* transform(service, { hooked: constant("original") }, { codemode: false })
yield* hooks.register("tool", "execute.after", (event) =>
Effect.sync(() => {
if (event.status !== "completed") return
event.result = {
...event.result,
content: [
{ type: "file", uri: "data:image/png;base64,aW1hZ2U=", mime: "image/png", name: "hook.png" },
],
}
}),
)
expect((yield* executeTool(service, call("hooked"))).content).toEqual([
{
type: "file",
uri: "data:image/jpeg;base64,aW1hZ2Ugbm9ybWFsaXplZA==",
mime: "image/jpeg",
name: "hook.png",
},
])
}),
)
it.effect("publishes progress metadata unchanged", () =>
Effect.gen(function* () {
const service = yield* Tool.Service