Compare commits

...

1 Commits

Author SHA1 Message Date
Kit Langton ed35a8427d fix(core): preserve admitted tool names 2026-08-07 23:38:16 -04:00
5 changed files with 32 additions and 20 deletions
+1 -1
View File
@@ -155,7 +155,7 @@ function runtime(
) {
const tools: Record<string, Tool.Tool<never>> = {}
for (const [name, registration] of registrations) {
const child = definition(registration)
const child = definition(name, registration)
const path = qualifiedName(registration)
tools[path] = Tool.make({
description: child.description,
+2 -2
View File
@@ -213,8 +213,8 @@ const layer = Layer.effect(
definitions: [
...Array.from(direct)
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
.map(([, tool]) => definition(tool)),
...(codemodeTool ? [definition(codemodeTool)] : []),
.map(([name, tool]) => definition(name, tool)),
...(codemodeTool ? [definition("execute", codemodeTool)] : []),
],
execute: (input: {
readonly sessionID: SessionSchema.ID
+2 -9
View File
@@ -3,8 +3,8 @@ import { Tool } from "@opencode-ai/schema/tool"
import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec"
import { Effect, JsonSchema, Schema } from "effect"
export const definition = (tool: Tool.Info<any, any>): ToolDefinition => ({
name: effectiveName(tool),
export const definition = (name: string, tool: Tool.Info<any, any>): ToolDefinition => ({
name,
description: tool.description,
inputSchema: inputJsonSchema(tool.input),
...(tool.output === undefined ? {} : { outputSchema: outputJsonSchema(tool.output) }),
@@ -199,10 +199,3 @@ const stringify = (value: unknown) => {
return String(value)
}
}
const normalizedName = (tool: Tool.Info) => tool.name.replace(/[^a-zA-Z0-9_-]/g, "_")
const effectiveName = (tool: Tool.Info) =>
tool.options?.namespace === undefined
? normalizedName(tool)
: `${tool.options.namespace.replaceAll(".", "_")}_${normalizedName(tool)}`
@@ -146,6 +146,25 @@ describe("Tool", () => {
}),
)
it.effect("advertises the admitted direct lookup identity", () =>
Effect.gen(function* () {
const service = yield* Tool.Service
const tool = {
...make(),
name: "send.message",
options: { namespace: "slack.admin", codemode: false },
}
yield* service.transform((draft) => draft.add(tool))
tool.name = "renamed"
const snapshot = yield* service.snapshot()
expect(snapshot.definitions.map((definition) => definition.name)).toEqual(["slack_admin_send_message", "execute"])
expect((yield* snapshot.execute(call("slack_admin_send_message"))).content).toEqual([
{ type: "text", text: "slack_admin_send_message" },
])
}),
)
it.effect("snapshots external tools with missing input schemas", () =>
Effect.gen(function* () {
const service = yield* Tool.Service
+8 -8
View File
@@ -14,7 +14,7 @@ test("tools are structural values", async () => {
}
const tool: Info = config
expect(definition(tool)).toEqual({
expect(definition(tool.name, tool)).toEqual({
name: "foreign",
description: "Foreign tool",
inputSchema: {
@@ -43,7 +43,7 @@ test("Effect tool schemas use exact optional keys and flatten compatible constra
execute: () => Effect.succeed({ content: "unused" }),
}
expect(definition(tool).inputSchema).toEqual({
expect(definition(tool.name, tool).inputSchema).toEqual({
type: "object",
properties: {
offset: { type: "integer", minimum: 0 },
@@ -63,7 +63,7 @@ test("Effect tool schemas inline named child schemas", () => {
execute: () => Effect.succeed({ content: "unused" }),
}
expect(definition(tool).inputSchema).toEqual({
expect(definition(tool.name, tool).inputSchema).toEqual({
type: "object",
properties: {
child: {
@@ -89,8 +89,8 @@ test("Effect tool schemas resolve escaped definition names", () => {
execute: () => Effect.succeed({ content: "unused" }),
}
expect(JSON.stringify(definition(tool).inputSchema)).not.toContain("$ref")
expect(JSON.stringify(definition(tool).inputSchema)).not.toContain("$defs")
expect(JSON.stringify(definition(tool.name, tool).inputSchema)).not.toContain("$ref")
expect(JSON.stringify(definition(tool.name, tool).inputSchema)).not.toContain("$defs")
})
test("portable schemas validate and describe typed tools", async () => {
@@ -129,7 +129,7 @@ test("portable schemas validate and describe typed tools", async () => {
execute: ({ count }) => Effect.succeed({ output: count + 1 }),
})
expect(definition(tool)).toEqual({
expect(definition(tool.name, tool)).toEqual({
name: "portable",
description: "Portable tool",
inputSchema: { type: "object", properties: { count: { type: "string" } } },
@@ -194,7 +194,7 @@ test("raw JSON schemas are render-only and omitted output means model-only", asy
execute: (input) => Effect.succeed({ content: JSON.stringify(input) }),
})
expect(definition(tool)).toEqual({
expect(definition(tool.name, tool)).toEqual({
name: "raw",
description: "Raw tool",
inputSchema: { type: "object", properties: { value: { type: "string" } } },
@@ -213,7 +213,7 @@ test("missing external input schemas fall back to an empty schema", () => {
execute: () => Effect.succeed({ content: "unused" }),
} as unknown as Info
expect(definition(tool)).toEqual({
expect(definition(tool.name, tool)).toEqual({
name: "external",
description: "External tool",
inputSchema: {},