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
|
|
@ -3,8 +3,12 @@ import { Tool } from "@opencode-ai/core/public"
|
|||
import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
|
||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||
import { executeTool, settleTool, toolDefinitions } from "./lib/tool"
|
||||
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
||||
import { Tools } from "@opencode-ai/core/tool/tools"
|
||||
import { Effect, Exit, Layer, Schema, Scope } from "effect"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
|
|
@ -20,11 +24,13 @@ const registry = ToolRegistry.layer.pipe(
|
|||
const it = testEffect(Layer.mergeAll(applications, registry))
|
||||
|
||||
const sessionID = SessionV2.ID.make("ses_application_tool")
|
||||
const agent = AgentV2.ID.make("build")
|
||||
const assistantMessageID = SessionMessage.ID.make("msg_application_tool")
|
||||
const contextual = (contexts: Tool.Context[]) =>
|
||||
Tool.make({
|
||||
description: "Read application context",
|
||||
parameters: Schema.Struct({ query: Schema.String }),
|
||||
success: Schema.Struct({ answer: Schema.String }),
|
||||
input: Schema.Struct({ query: Schema.String }),
|
||||
output: Schema.Struct({ answer: Schema.String }),
|
||||
execute: ({ query }, context) =>
|
||||
Effect.sync(() => {
|
||||
contexts.push(context)
|
||||
|
|
@ -37,23 +43,69 @@ const contextual = (contexts: Tool.Context[]) =>
|
|||
})
|
||||
|
||||
describe("ApplicationTools", () => {
|
||||
it.effect("keeps the Core carrier opaque and executes its single handler", () =>
|
||||
Effect.gen(function* () {
|
||||
const applications = yield* ApplicationTools.Service
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const contexts: Tool.Context[] = []
|
||||
const tool = contextual(contexts)
|
||||
expect(Object.keys(tool)).toEqual([])
|
||||
|
||||
yield* applications.register({ opaque: tool })
|
||||
expect(
|
||||
yield* executeTool(registry, {
|
||||
sessionID,
|
||||
agent,
|
||||
assistantMessageID,
|
||||
call: { type: "tool-call", id: "call-opaque", name: "opaque", input: { query: "once" } },
|
||||
}),
|
||||
).toEqual({
|
||||
type: "content",
|
||||
value: [
|
||||
{ type: "text", text: "ONCE" },
|
||||
{ type: "media", mediaType: "image/png", data: "aGVsbG8=", filename: "result.png" },
|
||||
],
|
||||
})
|
||||
expect(contexts).toEqual([{ sessionID, agent, assistantMessageID, toolCallID: "call-opaque" }])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("exposes narrow scoped Location registration and validates names", () =>
|
||||
Effect.gen(function* () {
|
||||
const tools: Tools.Interface = yield* Tools.Service
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const scope = yield* Scope.make()
|
||||
|
||||
yield* tools.register({ location_tool: contextual([]) }).pipe(Scope.provide(scope))
|
||||
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["location_tool"])
|
||||
expect(yield* Effect.flip(tools.register({ "invalid name": contextual([]) }))).toBeInstanceOf(
|
||||
Tool.RegistrationError,
|
||||
)
|
||||
|
||||
yield* Scope.close(scope, Exit.void)
|
||||
expect(yield* toolDefinitions(registry)).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("filters an application tool by its name without adding execution authorization", () =>
|
||||
Effect.gen(function* () {
|
||||
const applications = yield* ApplicationTools.Service
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const contexts: Tool.Context[] = []
|
||||
yield* applications.attach({ application_context: contextual(contexts) })
|
||||
yield* applications.register({ application_context: contextual(contexts) })
|
||||
|
||||
expect(yield* registry.definitions([{ action: "application_context", resource: "*", effect: "deny" }])).toEqual(
|
||||
[],
|
||||
)
|
||||
expect(
|
||||
yield* registry.settle({
|
||||
yield* toolDefinitions(registry, [{ action: "application_context", resource: "*", effect: "deny" }]),
|
||||
).toEqual([])
|
||||
expect(
|
||||
yield* settleTool(registry, {
|
||||
sessionID,
|
||||
agent,
|
||||
assistantMessageID,
|
||||
call: { type: "tool-call", id: "call-denied", name: "application_context", input: { query: "hello" } },
|
||||
}),
|
||||
).toMatchObject({ result: { type: "content" } })
|
||||
expect(contexts).toEqual([{ sessionID, id: "call-denied", name: "application_context" }])
|
||||
expect(contexts).toEqual([{ sessionID, agent, assistantMessageID, toolCallID: "call-denied" }])
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -63,14 +115,16 @@ describe("ApplicationTools", () => {
|
|||
const registry = yield* ToolRegistry.Service
|
||||
const contexts: Tool.Context[] = []
|
||||
|
||||
yield* applications.attach({ application_context: contextual(contexts) })
|
||||
yield* applications.register({ application_context: contextual(contexts) })
|
||||
|
||||
expect(yield* registry.definitions()).toMatchObject([
|
||||
expect(yield* toolDefinitions(registry)).toMatchObject([
|
||||
{ name: "application_context", description: "Read application context" },
|
||||
])
|
||||
expect(
|
||||
yield* registry.settle({
|
||||
yield* settleTool(registry, {
|
||||
sessionID,
|
||||
agent,
|
||||
assistantMessageID,
|
||||
call: { type: "tool-call", id: "call-context", name: "application_context", input: { query: "hello" } },
|
||||
}),
|
||||
).toEqual({
|
||||
|
|
@ -82,14 +136,14 @@ describe("ApplicationTools", () => {
|
|||
],
|
||||
},
|
||||
output: {
|
||||
structured: { answer: "HELLO" },
|
||||
structured: {},
|
||||
content: [
|
||||
{ type: "text", text: "HELLO" },
|
||||
{ type: "file", source: { type: "data", data: "aGVsbG8=" }, mime: "image/png", name: "result.png" },
|
||||
],
|
||||
},
|
||||
})
|
||||
expect(contexts).toEqual([{ sessionID, id: "call-context", name: "application_context" }])
|
||||
expect(contexts).toEqual([{ sessionID, agent, assistantMessageID, toolCallID: "call-context" }])
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -99,11 +153,11 @@ describe("ApplicationTools", () => {
|
|||
const registry = yield* ToolRegistry.Service
|
||||
const scope = yield* Scope.make()
|
||||
|
||||
yield* applications.attach({ temporary: contextual([]) }).pipe(Scope.provide(scope))
|
||||
expect((yield* registry.definitions()).map((tool) => tool.name)).toEqual(["temporary"])
|
||||
yield* applications.register({ temporary: contextual([]) }).pipe(Scope.provide(scope))
|
||||
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["temporary"])
|
||||
|
||||
yield* Scope.close(scope, Exit.void)
|
||||
expect(yield* registry.definitions()).toEqual([])
|
||||
expect(yield* toolDefinitions(registry)).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -112,13 +166,15 @@ describe("ApplicationTools", () => {
|
|||
const applications = yield* ApplicationTools.Service
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const attachmentScope = yield* Scope.make()
|
||||
yield* applications.attach({ contextual: contextual([]) }).pipe(Scope.provide(attachmentScope))
|
||||
expect((yield* registry.definitions()).map((tool) => tool.name)).toEqual(["contextual"])
|
||||
yield* applications.register({ contextual: contextual([]) }).pipe(Scope.provide(attachmentScope))
|
||||
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["contextual"])
|
||||
|
||||
yield* Scope.close(attachmentScope, Exit.void)
|
||||
expect(
|
||||
yield* registry.settle({
|
||||
yield* settleTool(registry, {
|
||||
sessionID,
|
||||
agent,
|
||||
assistantMessageID,
|
||||
call: { type: "tool-call", id: "call-removed", name: "contextual", input: { query: "hello" } },
|
||||
}),
|
||||
).toEqual({ result: { type: "error", value: "Unknown tool: contextual" } })
|
||||
|
|
@ -132,9 +188,9 @@ describe("ApplicationTools", () => {
|
|||
const scope = yield* Scope.make()
|
||||
yield* Scope.close(scope, Exit.void)
|
||||
|
||||
yield* applications.attach({ closed: contextual([]) }).pipe(Scope.provide(scope))
|
||||
yield* applications.register({ closed: contextual([]) }).pipe(Scope.provide(scope))
|
||||
|
||||
expect(yield* registry.definitions()).toEqual([])
|
||||
expect(yield* toolDefinitions(registry)).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -143,12 +199,12 @@ describe("ApplicationTools", () => {
|
|||
const applications = yield* ApplicationTools.Service
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const attached = { stable: contextual([]) }
|
||||
yield* applications.attach(attached)
|
||||
yield* applications.register(attached)
|
||||
Object.assign(attached, { late: contextual([]) })
|
||||
|
||||
yield* Effect.scoped(applications.attach({ temporary: contextual([]) }))
|
||||
yield* Effect.scoped(applications.register({ temporary: contextual([]) }))
|
||||
|
||||
expect((yield* registry.definitions()).map((tool) => tool.name)).toEqual(["stable"])
|
||||
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["stable"])
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -159,22 +215,26 @@ describe("ApplicationTools", () => {
|
|||
const firstContexts: Tool.Context[] = []
|
||||
const secondContexts: Tool.Context[] = []
|
||||
const scope = yield* Scope.make()
|
||||
yield* applications.attach({ contextual: contextual(firstContexts) })
|
||||
expect((yield* registry.definitions()).map((tool) => tool.name)).toEqual(["contextual"])
|
||||
yield* applications.attach({ contextual: contextual(secondContexts) }).pipe(Scope.provide(scope))
|
||||
yield* applications.register({ contextual: contextual(firstContexts) })
|
||||
expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["contextual"])
|
||||
yield* applications.register({ contextual: contextual(secondContexts) }).pipe(Scope.provide(scope))
|
||||
|
||||
yield* registry.settle({
|
||||
yield* settleTool(registry, {
|
||||
sessionID,
|
||||
agent,
|
||||
assistantMessageID,
|
||||
call: { type: "tool-call", id: "call-second", name: "contextual", input: { query: "second" } },
|
||||
})
|
||||
yield* Scope.close(scope, Exit.void)
|
||||
yield* registry.settle({
|
||||
yield* settleTool(registry, {
|
||||
sessionID,
|
||||
agent,
|
||||
assistantMessageID,
|
||||
call: { type: "tool-call", id: "call-first", name: "contextual", input: { query: "first" } },
|
||||
})
|
||||
|
||||
expect(secondContexts).toEqual([{ sessionID, id: "call-second", name: "contextual" }])
|
||||
expect(firstContexts).toEqual([{ sessionID, id: "call-first", name: "contextual" }])
|
||||
expect(secondContexts).toEqual([{ sessionID, agent, assistantMessageID, toolCallID: "call-second" }])
|
||||
expect(firstContexts).toEqual([{ sessionID, agent, assistantMessageID, toolCallID: "call-first" }])
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -182,32 +242,26 @@ describe("ApplicationTools", () => {
|
|||
Effect.gen(function* () {
|
||||
const applications = yield* ApplicationTools.Service
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const transform = yield* registry.transform()
|
||||
const locationContexts: Tool.Context[] = []
|
||||
const applicationContexts: Tool.Context[] = []
|
||||
const location = contextual(locationContexts)
|
||||
yield* transform((editor) =>
|
||||
editor.set("shared", {
|
||||
tool: location.definition,
|
||||
permission: { action: "question", resource: "*" },
|
||||
execute: ({ parameters, sessionID, call }) =>
|
||||
location.execute(parameters, { sessionID, id: call.id, name: call.name }),
|
||||
}),
|
||||
)
|
||||
yield* applications.attach({ shared: contextual(applicationContexts) })
|
||||
yield* registry.register({ shared: location })
|
||||
yield* applications.register({ shared: contextual(applicationContexts) })
|
||||
|
||||
expect(
|
||||
(yield* registry.definitions([{ action: "question", resource: "*", effect: "deny" }])).map(
|
||||
(yield* toolDefinitions(registry, [{ action: "shared", resource: "*", effect: "deny" }])).map(
|
||||
(definition) => definition.name,
|
||||
),
|
||||
).toEqual([])
|
||||
expect(
|
||||
yield* registry.settle({
|
||||
yield* settleTool(registry, {
|
||||
sessionID,
|
||||
agent,
|
||||
assistantMessageID,
|
||||
call: { type: "tool-call", id: "call-shared", name: "shared", input: { query: "location" } },
|
||||
}),
|
||||
).toMatchObject({ result: { type: "content" } })
|
||||
expect(locationContexts).toEqual([{ sessionID, id: "call-shared", name: "shared" }])
|
||||
expect(locationContexts).toEqual([{ sessionID, agent, assistantMessageID, toolCallID: "call-shared" }])
|
||||
expect(applicationContexts).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue