refactor(core): separate Code Mode host wiring
This commit is contained in:
parent
e113aad5e0
commit
42b63d6660
8 changed files with 190 additions and 93 deletions
|
|
@ -50,13 +50,13 @@ export interface CodeModeTools {
|
|||
export const create = (options: {
|
||||
readonly registrations: ReadonlyMap<string, Registration>
|
||||
readonly current: (name: string) => Registration | undefined
|
||||
readonly tools?: CodeModeTools
|
||||
readonly tools: CodeModeTools
|
||||
}) => {
|
||||
const runtime = (
|
||||
invoke: (name: string, registration: Registration, input: unknown) => Effect.Effect<unknown, unknown>,
|
||||
hooks?: CodeMode.ToolCallHooks,
|
||||
) => {
|
||||
const tools: CodeModeTools = Object.assign(Object.create(null), options.tools)
|
||||
const tools = cloneTools(options.tools)
|
||||
for (const [name, registration] of options.registrations) {
|
||||
const child = definition(name, registration.tool)
|
||||
const value = Tool.make({
|
||||
|
|
@ -168,6 +168,15 @@ export const create = (options: {
|
|||
})
|
||||
}
|
||||
|
||||
function cloneTools(tools: CodeModeTools): CodeModeTools {
|
||||
return Object.assign(
|
||||
Object.create(null),
|
||||
Object.fromEntries(
|
||||
Object.entries(tools).map(([name, value]) => [name, Tool.isDefinition(value) ? value : cloneTools(value)]),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function displayInput(input: unknown): Record<string, unknown> | undefined {
|
||||
if (input === null || input === undefined) return
|
||||
if (typeof input !== "object" || Array.isArray(input)) return { input }
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { definition, permission, registrationEntries, RegistrationError, settle,
|
|||
import { Tools } from "./tools"
|
||||
import { ToolHooks } from "./hooks"
|
||||
import { makeLocationNode } from "../effect/app-node"
|
||||
import { LayerNode } from "../effect/layer-node"
|
||||
import { SessionError } from "@opencode-ai/schema/session-error"
|
||||
import { toSessionError } from "../session/to-session-error"
|
||||
|
||||
|
|
@ -52,10 +53,16 @@ export interface Settlement {
|
|||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/ToolRegistry") {}
|
||||
class CodeModeCatalog extends Context.Service<CodeModeCatalog, { readonly tools?: CodeModeTools }>()(
|
||||
class CodeModeCatalog extends Context.Service<CodeModeCatalog, { readonly tools: CodeModeTools }>()(
|
||||
"@opencode/v2/CodeModeCatalog",
|
||||
) {}
|
||||
|
||||
const codeModeCatalogNode = makeLocationNode({
|
||||
service: CodeModeCatalog,
|
||||
layer: Layer.succeed(CodeModeCatalog, CodeModeCatalog.of({ tools: {} })),
|
||||
deps: [],
|
||||
})
|
||||
|
||||
const registryLayer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
|
|
@ -209,10 +216,9 @@ const registryLayer = Layer.effect(
|
|||
}
|
||||
const direct = new Map(Array.from(registrations).filter(([, registration]) => !registration.deferred))
|
||||
const deferred = new Map(Array.from(registrations).filter(([, registration]) => registration.deferred))
|
||||
const tools = Flag.CODEMODE_ENABLED ? codeModeTools : undefined
|
||||
const tools = Flag.CODEMODE_ENABLED ? codeModeTools : {}
|
||||
const execute =
|
||||
(deferred.size > 0 || (tools !== undefined && Object.keys(tools).length > 0)) &&
|
||||
!whollyDisabled("execute", input.permissions ?? [])
|
||||
(deferred.size > 0 || Object.keys(tools).length > 0) && !whollyDisabled("execute", input.permissions ?? [])
|
||||
? ExecuteTool.create({
|
||||
registrations: deferred,
|
||||
current: (name) => local.get(name)?.at(-1)?.registration,
|
||||
|
|
@ -239,37 +245,28 @@ const registryLayer = Layer.effect(
|
|||
}),
|
||||
)
|
||||
|
||||
const makeLayer = (codeModeTools?: CodeModeTools) => {
|
||||
return Layer.effect(
|
||||
Tools.Service,
|
||||
Service.use((registry) => Effect.succeed(Tools.Service.of({ register: registry.register }))),
|
||||
).pipe(
|
||||
Layer.provideMerge(registryLayer),
|
||||
Layer.provide(Layer.succeed(CodeModeCatalog, CodeModeCatalog.of({ tools: codeModeTools }))),
|
||||
)
|
||||
}
|
||||
const layer = Layer.effect(
|
||||
Tools.Service,
|
||||
Service.use((registry) => Effect.succeed(Tools.Service.of({ register: registry.register }))),
|
||||
).pipe(Layer.provideMerge(registryLayer))
|
||||
|
||||
function whollyDisabled(action: string, rules: PermissionV2.Ruleset) {
|
||||
const rule = rules.findLast((rule) => Wildcard.match(action, rule.action))
|
||||
return rule?.resource === "*" && rule.effect === "deny"
|
||||
}
|
||||
|
||||
export function nodes(codeModeTools?: CodeModeTools) {
|
||||
const layer = makeLayer(codeModeTools)
|
||||
return {
|
||||
node: makeLocationNode({
|
||||
service: Service,
|
||||
layer,
|
||||
deps: [ToolOutputStore.node, ToolHooks.node],
|
||||
}),
|
||||
toolsNode: makeLocationNode({
|
||||
service: Tools.Service,
|
||||
layer,
|
||||
deps: [ToolOutputStore.node, ToolHooks.node],
|
||||
}),
|
||||
}
|
||||
export function codeModeReplacement(tools: CodeModeTools): LayerNode.Replacement {
|
||||
return [codeModeCatalogNode, Layer.succeed(CodeModeCatalog, CodeModeCatalog.of({ tools }))]
|
||||
}
|
||||
|
||||
const defaults = nodes()
|
||||
export const node = defaults.node
|
||||
export const toolsNode = defaults.toolsNode
|
||||
export const node = makeLocationNode({
|
||||
service: Service,
|
||||
layer,
|
||||
deps: [ToolOutputStore.node, ToolHooks.node, codeModeCatalogNode],
|
||||
})
|
||||
|
||||
export const toolsNode = makeLocationNode({
|
||||
service: Tools.Service,
|
||||
layer,
|
||||
deps: [ToolOutputStore.node, ToolHooks.node, codeModeCatalogNode],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const outputStore = Layer.mock(ToolOutputStore.Service, {
|
|||
})
|
||||
const registryLayer = AppNodeBuilder.build(ToolRegistry.node, [[ToolOutputStore.node, outputStore]])
|
||||
const it = testEffect(registryLayer)
|
||||
const codeModeNodes = ToolRegistry.nodes({
|
||||
const codeModeTools: ToolRegistry.CodeModeTools = {
|
||||
opencode: {
|
||||
v2: {
|
||||
health: {
|
||||
|
|
@ -44,8 +44,13 @@ const codeModeNodes = ToolRegistry.nodes({
|
|||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
const codeModeIt = testEffect(AppNodeBuilder.build(codeModeNodes.node, [[ToolOutputStore.node, outputStore]]))
|
||||
}
|
||||
const codeModeIt = testEffect(
|
||||
AppNodeBuilder.build(ToolRegistry.node, [
|
||||
[ToolOutputStore.node, outputStore],
|
||||
ToolRegistry.codeModeReplacement(codeModeTools),
|
||||
]),
|
||||
)
|
||||
const identity = {
|
||||
agent: AgentV2.ID.make("build"),
|
||||
assistantMessageID: SessionMessage.ID.make("msg_registry"),
|
||||
|
|
@ -91,6 +96,28 @@ describe("ToolRegistry", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
codeModeIt.effect("keeps host Code Mode trees immutable while merging deferred tools", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* ToolRegistry.Service
|
||||
yield* service.register({ echo: make() }, { group: "opencode", deferred: true })
|
||||
|
||||
expect((yield* toolDefinitions(service))[0]?.description).toContain("tools.opencode.echo")
|
||||
expect((yield* toolDefinitions(service))[0]?.description).toContain("tools.opencode.echo")
|
||||
expect(
|
||||
yield* executeTool(service, {
|
||||
sessionID,
|
||||
...identity,
|
||||
call: {
|
||||
type: "tool-call",
|
||||
id: "call-opencode-echo",
|
||||
name: "execute",
|
||||
input: { code: 'return await tools.opencode.echo({ text: "hello" })' },
|
||||
},
|
||||
}),
|
||||
).toEqual({ type: "text", value: '{\n "text": "hello"\n}' })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("filters disabled tools with edit aliases and ordered wildcard precedence", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* ToolRegistry.Service
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue