refactor(core): consolidate tool architecture

This commit is contained in:
Dax Raad 2026-07-26 20:08:55 -04:00
commit 8db7487c89
466 changed files with 9405 additions and 11071 deletions

View file

@ -4,13 +4,11 @@ import { ToolFailure } from "@opencode-ai/ai"
import { McpEvent } from "@opencode-ai/schema/mcp-event"
import { Effect, Exit, type JsonSchema, Layer, Scope, Semaphore, Stream } from "effect"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { EventV2 } from "../event"
import { Bus } from "../bus"
import { MCP } from "../mcp"
import { PermissionV2 } from "../permission"
import { Tool } from "./tool"
import { Tools } from "./tools"
import { ToolRegistry } from "./registry"
import { Permission } from "../permission"
import { Tool } from "../tool"
/**
* Registry namespace and permission action names for MCP tools.
@ -21,9 +19,9 @@ export const name = (server: string, tool: string) => `${namespace(server)}_${to
export const layer = Layer.effectDiscard(
Effect.gen(function* () {
const mcp = yield* MCP.Service
const tools = yield* Tools.Service
const events = yield* EventV2.Service
const permission = yield* PermissionV2.Service
const tools = yield* Tool.Service
const bus = yield* Bus.Service
const permission = yield* Permission.Service
const scope = yield* Scope.Scope
const lock = Semaphore.makeUnsafe(1)
let current: Scope.Closeable | undefined
@ -32,27 +30,25 @@ export const layer = Layer.effectDiscard(
// registry never has a gap where MCP tools disappear mid-swap.
const reconcile = lock.withPermit(
Effect.gen(function* () {
const groups = new Map<
string,
{
tools: Record<string, Tool.Any>
codemode: boolean
}
>()
for (const tool of yield* mcp.tools()) {
const group = groups.get(tool.server) ?? { tools: {}, codemode: tool.codemode !== false }
const schema = (tool.inputSchema ?? {}) as JsonSchema.JsonSchema
group.tools[tool.name] = Tool.make({
description: tool.description ?? "",
input: {
...schema,
type: "object",
properties: schema.properties ?? {},
additionalProperties: false,
},
output: (tool.outputSchema ?? {}) as JsonSchema.JsonSchema,
execute: (input, context) =>
Effect.gen(function* () {
const discovered = yield* mcp.tools()
const next = yield* Scope.fork(scope)
yield* tools
.transform((draft) => {
for (const tool of discovered) {
const schema = (tool.inputSchema ?? {}) as JsonSchema.JsonSchema
draft.add({
name: tool.name,
options: { namespace: namespace(tool.server), codemode: tool.codemode !== false },
description: tool.description ?? "",
input: {
...schema,
type: "object",
properties: schema.properties ?? {},
additionalProperties: false,
},
output: (tool.outputSchema ?? {}) as JsonSchema.JsonSchema,
execute: (input, context) =>
Effect.gen(function* () {
yield* permission.assert({
action: name(tool.server, tool.name),
resources: ["*"],
@ -90,31 +86,27 @@ export const layer = Layer.effectDiscard(
const content = result.content.map((part) =>
part.type === "text"
? { type: "text" as const, text: part.text }
: { type: "file" as const, data: part.data, mime: part.mimeType },
: {
type: "file" as const,
uri: `data:${part.mimeType};base64,${part.data}`,
mime: part.mimeType,
},
)
const text = content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n")
return {
output: result.structured ?? (text === "" ? null : text),
...(content.length === 0 ? {} : { content: content as [Tool.Content, ...Tool.Content[]] }),
...(content.length === 0 ? {} : { content }),
}
}).pipe(
Effect.mapError((error) =>
error instanceof ToolFailure
? error
: new ToolFailure({ message: `Unable to execute ${name(tool.server, tool.name)}` }),
),
),
}).pipe(
Effect.mapError((error) =>
error instanceof ToolFailure
? error
: new ToolFailure({ message: `Unable to execute ${name(tool.server, tool.name)}` }),
),
),
})
}
})
groups.set(tool.server, group)
}
const next = yield* Scope.fork(scope)
yield* tools
.registerBatch(
Array.from(groups, ([server, group]) => ({
tools: group.tools,
options: { namespace: namespace(server), codemode: group.codemode },
})),
)
.pipe(Scope.provide(next), Effect.orDie)
if (current) yield* Scope.close(current, Exit.void)
current = next
@ -122,7 +114,7 @@ export const layer = Layer.effectDiscard(
)
yield* reconcile.pipe(Effect.forkScoped)
yield* events.subscribe(McpEvent.ToolsChanged).pipe(
yield* bus.subscribe(McpEvent.ToolsChanged).pipe(
Stream.runForEach(() => reconcile),
Effect.forkScoped({ startImmediately: true }),
)
@ -132,5 +124,5 @@ export const layer = Layer.effectDiscard(
export const node = makeLocationNode({
name: "mcp-tools",
layer,
deps: [ToolRegistry.toolsNode, MCP.node, EventV2.node, PermissionV2.node],
deps: [Tool.node, MCP.node, Bus.node, Permission.node],
})