mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
refactor(ai): simplify tool name lowering
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user