refactor(ai): simplify tool name lowering

This commit is contained in:
Aiden Cline
2026-09-29 21:25:17 -05:00
parent 5310957aff
commit dffcd7767b
+53 -74
View File
@@ -1,12 +1,13 @@
import { Effect } from "effect" import { Effect } from "effect"
import { import {
AIError,
LLMRequest, LLMRequest,
Message, Message,
ToolDefinition, ToolDefinition,
type ContentPart,
type LLMEvent, type LLMEvent,
type ToolCallPart,
type ToolEntry, type ToolEntry,
type ToolNamespace, type ToolResultPart,
} from "./schema/index.js" } from "./schema/index.js"
import { ProviderShared } from "./protocols/shared.js" import { ProviderShared } from "./protocols/shared.js"
@@ -20,95 +21,73 @@ import { ProviderShared } from "./protocols/shared.js"
*/ */
export type NamespaceStyle = "flat" | "native" export type NamespaceStyle = "flat" | "native"
interface ToolName { interface Name {
readonly namespace?: string readonly namespace?: string
readonly name: string readonly name: string
} }
interface Leaf { /** Lower declared tool names to wire names, and return `raise` to map tool events back. */
readonly namespace?: string
readonly tool: ToolDefinition
}
/**
* Lower declared tool names in a request to the protocol's wire names, and
* return the inverse for the protocol's tool events.
*/
export const lower = Effect.fn("ToolNames.lower")(function* (request: LLMRequest, style: NamespaceStyle) { export const lower = Effect.fn("ToolNames.lower")(function* (request: LLMRequest, style: NamespaceStyle) {
const leaves = yield* collect(request.tools, []) const names = new Map<string, Name>()
const names = leaves.map((leaf) => { for (const leaf of walk(request.tools)) {
const declared = { namespace: leaf.namespace, name: leaf.tool.name } const bad = leaf.path.find((part) => part.includes("."))
return { declared, wire: key(wire(declared, style)) } if (bad !== undefined) return yield* ProviderShared.invalidRequest(`Tool namespace "${bad}" must not contain "."`)
}) const name = { namespace: leaf.path.join(".") || undefined, name: leaf.tool.name }
const collision = names.flatMap((entry, index) => const key = id(wire(name, style))
names const taken = names.get(key)
.slice(0, index) if (taken)
.filter((other) => other.wire === entry.wire) return yield* ProviderShared.invalidRequest(
.map((other) => [other, entry] as const), `Tools "${id(taken)}" and "${id(name)}" both use the provider tool name "${key}"`,
)[0] )
if (collision !== undefined) names.set(key, name)
return yield* ProviderShared.invalidRequest( }
`Tools "${key(collision[0].declared)}" and "${key(collision[1].declared)}" both use the provider tool name "${collision[1].wire}"`,
) const named = (part: ContentPart): part is ToolCallPart | ToolResultPart =>
const declared = new Map(names.map((entry) => [entry.wire, entry.declared])) (part.type === "tool-call" || part.type === "tool-result") && part.namespace !== undefined
return { return {
request: LLMRequest.update(request, { request: LLMRequest.update(request, {
tools: style === "flat" ? leaves.map((leaf) => rename(leaf, style)) : yield* nativeTools(request.tools), // Native namespaces are one level deep, so deeper levels join into the leaf name.
messages: lowerMessages(request.messages, style), tools:
style === "flat"
? flatten(request.tools)
: request.tools.map((tool) => (tool.type === "tool" ? tool : { ...tool, tools: flatten(tool.tools) })),
messages: request.messages.map((msg) =>
msg.content.some(named)
? new Message({
...msg,
content: msg.content.map((part) => (named(part) ? { ...part, ...wire(part, style) } : part)),
})
: msg,
),
}), }),
raise: (event: LLMEvent): LLMEvent => { raise: (event: LLMEvent): LLMEvent => {
if (!("name" in event)) return event const name = "name" in event ? names.get(id(event)) : undefined
const name = declared.get(key(event)) return name ? { ...event, ...name } : event
return name === undefined ? event : { ...event, namespace: name.namespace, name: name.name }
}, },
} }
}) })
const collect = (tools: ReadonlyArray<ToolEntry>, path: ReadonlyArray<string>): Effect.Effect<Leaf[], AIError> => const walk = (
Effect.forEach(tools, (tool) => { tools: ReadonlyArray<ToolEntry>,
if (tool.type === "tool") path: ReadonlyArray<string> = [],
return Effect.succeed([{ namespace: path.length === 0 ? undefined : path.join("."), tool }]) ): Array<{ readonly path: ReadonlyArray<string>; readonly tool: ToolDefinition }> =>
if (tool.name.includes(".")) tools.flatMap((tool) => (tool.type === "tool" ? [{ path, tool }] : walk(tool.tools, [...path, tool.name])))
return ProviderShared.invalidRequest(`Tool namespace "${tool.name}" must not contain "."`)
return collect(tool.tools, [...path, tool.name])
}).pipe(Effect.map((groups) => groups.flat()))
// Native namespaces are one level deep, so deeper levels join into the leaf name. const flatten = (tools: ReadonlyArray<ToolEntry>) =>
const nativeTools = (tools: ReadonlyArray<ToolEntry>) => walk(tools).map((leaf) =>
Effect.forEach( leaf.path.length === 0
tools, ? leaf.tool
(tool): Effect.Effect<ToolEntry, AIError> => : new ToolDefinition({ ...leaf.tool, name: [...leaf.path, leaf.tool.name].join("_") }),
tool.type === "tool"
? Effect.succeed(tool)
: collect(tool.tools, [tool.name]).pipe(
Effect.map((leaves): ToolNamespace => ({ ...tool, tools: leaves.map((leaf) => rename(leaf, "native")) })),
),
) )
const lowerMessages = (messages: ReadonlyArray<Message>, style: NamespaceStyle) => const wire = (tool: Name, style: NamespaceStyle): Name => {
messages.map((message) => { const path = tool.namespace?.split(".") ?? []
const content = message.content.map((part) => { if (style === "native" && path.length > 0)
if ((part.type !== "tool-call" && part.type !== "tool-result") || part.namespace === undefined) return part return { namespace: path[0], name: [...path.slice(1), tool.name].join("_") }
const name = wire(part, style) return { namespace: undefined, name: [...path, tool.name].join("_") }
return { ...part, namespace: name.namespace, name: name.name }
})
return content.every((part, index) => part === message.content[index])
? message
: new Message({ ...message, content })
})
const rename = (leaf: Leaf, style: NamespaceStyle) =>
leaf.namespace === undefined
? leaf.tool
: new ToolDefinition({ ...leaf.tool, name: wire({ namespace: leaf.namespace, name: leaf.tool.name }, style).name })
const wire = (tool: ToolName, style: NamespaceStyle): ToolName => {
if (tool.namespace === undefined) return { name: tool.name }
const path = tool.namespace.split(".")
if (style === "flat") return { name: [...path, tool.name].join("_") }
return { namespace: path[0], name: [...path.slice(1), tool.name].join("_") }
} }
const key = (tool: ToolName) => (tool.namespace === undefined ? tool.name : `${tool.namespace}.${tool.name}`) const id = (tool: Name) => (tool.namespace ? `${tool.namespace}.${tool.name}` : tool.name)
export * as ToolNames from "./tool-names.js" export * as ToolNames from "./tool-names.js"