refactor(tools): unify tool APIs and result handling (#38367)
This commit is contained in:
parent
8cac010bac
commit
79c1544072
133 changed files with 3602 additions and 2770 deletions
|
|
@ -8,7 +8,7 @@ import {
|
|||
inputTypeScript,
|
||||
outputTypeScript,
|
||||
} from "./tool-schema.js"
|
||||
import { isDefinition as isToolDefinition, type Definition } from "./tool.js"
|
||||
import { isTool, type Tool } from "./tool.js"
|
||||
import type { Tools } from "./tools.js"
|
||||
import {
|
||||
CodeModeDate,
|
||||
|
|
@ -28,7 +28,7 @@ type ServicesOf<T, Depth extends ReadonlyArray<unknown>> = Depth["length"] exten
|
|||
? never
|
||||
: T extends {
|
||||
readonly _tag: "CodeModeTool"
|
||||
readonly run: (input: unknown) => Effect.Effect<unknown, unknown, infer R>
|
||||
readonly execute: (input: unknown) => Effect.Effect<unknown, unknown, infer R>
|
||||
}
|
||||
? R
|
||||
: T extends object
|
||||
|
|
@ -118,8 +118,6 @@ export class ToolRuntimeError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
const isDefinition = <R>(value: Definition<R> | Tools<R>): value is Definition<R> => isToolDefinition<R>(value)
|
||||
|
||||
const runHost = <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, ToolError, R> =>
|
||||
effect.pipe(
|
||||
Effect.catchCause((cause) => {
|
||||
|
|
@ -286,9 +284,9 @@ export const copyOut = (value: unknown, mode: CopyOutMode): unknown => {
|
|||
return value
|
||||
}
|
||||
|
||||
// Dots in tool names are namespace separators; the last definition for a canonical path wins.
|
||||
// Dots in tool names are namespace separators; the last tool for a canonical path wins.
|
||||
type ToolNode<R> = {
|
||||
definition?: Definition<R>
|
||||
tool?: Tool<R>
|
||||
readonly children: Map<string, ToolNode<R>>
|
||||
}
|
||||
|
||||
|
|
@ -303,7 +301,7 @@ const toolTrie = <R>(tools: Tools<R>): ToolNode<R> => {
|
|||
current.children.set(segment, child)
|
||||
current = child
|
||||
}
|
||||
if (isDefinition(value)) current.definition = value
|
||||
if (isTool<R>(value)) current.tool = value
|
||||
else insert(current, value)
|
||||
}
|
||||
}
|
||||
|
|
@ -314,25 +312,25 @@ const toolTrie = <R>(tools: Tools<R>): ToolNode<R> => {
|
|||
const canonicalSegments = (path: ReadonlyArray<string>): ReadonlyArray<string> =>
|
||||
path.flatMap((segment) => segment.split("."))
|
||||
|
||||
const definitions = <R>(
|
||||
const flattenTools = <R>(
|
||||
node: ToolNode<R>,
|
||||
path: ReadonlyArray<string> = [],
|
||||
): Array<{ path: string; definition: Definition<R> }> => [
|
||||
...(node.definition === undefined ? [] : [{ path: path.join("."), definition: node.definition }]),
|
||||
...Array.from(node.children, ([name, child]) => definitions(child, [...path, name])).flat(),
|
||||
): Array<{ path: string; tool: Tool<R> }> => [
|
||||
...(node.tool === undefined ? [] : [{ path: path.join("."), tool: node.tool }]),
|
||||
...Array.from(node.children, ([name, child]) => flattenTools(child, [...path, name])).flat(),
|
||||
]
|
||||
|
||||
const describeDefinition = <R>(path: string, definition: Definition<R>): ToolDescription => ({
|
||||
const describeTool = <R>(path: string, tool: Tool<R>): ToolDescription => ({
|
||||
path,
|
||||
description: definition.description,
|
||||
signature: `${toolExpression(path)}(input: ${inputTypeScript(definition, true)}): Promise<${outputTypeScript(definition, true)}>`,
|
||||
description: tool.description,
|
||||
signature: `${toolExpression(path)}(input: ${inputTypeScript(tool, true)}): Promise<${outputTypeScript(tool, true)}>`,
|
||||
})
|
||||
|
||||
const visibleDefinitions = <R>(tools: Tools<R>) =>
|
||||
definitions(toolTrie(tools)).map(({ path, definition }) => ({
|
||||
const visibleTools = <R>(tools: Tools<R>) =>
|
||||
flattenTools(toolTrie(tools)).map(({ path, tool }) => ({
|
||||
path,
|
||||
definition,
|
||||
description: describeDefinition(path, definition),
|
||||
tool,
|
||||
description: describeTool(path, tool),
|
||||
}))
|
||||
|
||||
export type DiscoveryPlan = {
|
||||
|
|
@ -361,12 +359,12 @@ const termForms = (term: string): Array<string> => {
|
|||
return forms
|
||||
}
|
||||
|
||||
const makeSearchTool = (searchIndex: ReadonlyArray<SearchEntry>): Definition => ({
|
||||
const makeSearchTool = (searchIndex: ReadonlyArray<SearchEntry>): Tool => ({
|
||||
_tag: "CodeModeTool",
|
||||
description: "Search available tools",
|
||||
input: SearchInput,
|
||||
output: SearchOutput,
|
||||
run: (input) =>
|
||||
execute: (input) =>
|
||||
Effect.sync(() => {
|
||||
const request = input as typeof SearchInput.Type
|
||||
const query = request.query ?? ""
|
||||
|
|
@ -422,8 +420,8 @@ const makeSearchTool = (searchIndex: ReadonlyArray<SearchEntry>): Definition =>
|
|||
})
|
||||
|
||||
const searchSignature = (() => {
|
||||
const definition = makeSearchTool([])
|
||||
return `search(input: ${inputTypeScript(definition, true)}): ${outputTypeScript(definition, true)}`
|
||||
const tool = makeSearchTool([])
|
||||
return `search(input: ${inputTypeScript(tool, true)}): ${outputTypeScript(tool, true)}`
|
||||
})()
|
||||
|
||||
const catalogLine = (tool: ToolDescription) => {
|
||||
|
|
@ -432,13 +430,13 @@ const catalogLine = (tool: ToolDescription) => {
|
|||
return description === "" ? ` - ${tool.signature}` : ` - ${tool.signature} // ${description}`
|
||||
}
|
||||
|
||||
const toSearchEntry = <R>(path: string, definition: Definition<R>, description: ToolDescription): SearchEntry => ({
|
||||
const toSearchEntry = <R>(path: string, tool: Tool<R>, description: ToolDescription): SearchEntry => ({
|
||||
description,
|
||||
namespace: path.split(".", 1)[0]!,
|
||||
searchText: [
|
||||
path,
|
||||
definition.description,
|
||||
...inputProperties(definition).flatMap(({ name, description: property }) =>
|
||||
tool.description,
|
||||
...inputProperties(tool).flatMap(({ name, description: property }) =>
|
||||
property === undefined ? [name] : [name, property],
|
||||
),
|
||||
]
|
||||
|
|
@ -447,14 +445,14 @@ const toSearchEntry = <R>(path: string, definition: Definition<R>, description:
|
|||
})
|
||||
|
||||
export const searchIndex = <R>(tools: Tools<R>): ReadonlyArray<SearchEntry> =>
|
||||
visibleDefinitions(tools).map(({ path, definition, description }) => toSearchEntry(path, definition, description))
|
||||
visibleTools(tools).map(({ path, tool, description }) => toSearchEntry(path, tool, description))
|
||||
|
||||
// Budget signatures round-robin so every namespace remains visible.
|
||||
export const prepare = <R>(tools: Tools<R>, catalogBudget = defaultCatalogBudget): DiscoveryPlan => {
|
||||
if (!Number.isSafeInteger(catalogBudget) || catalogBudget < 0) {
|
||||
throw new RangeError("discovery.catalogBudget must be a non-negative safe integer")
|
||||
}
|
||||
const visible = visibleDefinitions(tools)
|
||||
const visible = visibleTools(tools)
|
||||
const described = visible.map(({ description }) => description)
|
||||
|
||||
const namespaces = new Map<string, Array<ToolDescription>>()
|
||||
|
|
@ -589,7 +587,7 @@ export const prepare = <R>(tools: Tools<R>, catalogBudget = defaultCatalogBudget
|
|||
return {
|
||||
catalog: described,
|
||||
instructions: lines.join("\n"),
|
||||
searchIndex: visible.map(({ path, definition, description }) => toSearchEntry(path, definition, description)),
|
||||
searchIndex: visible.map(({ path, tool, description }) => toSearchEntry(path, tool, description)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -605,7 +603,7 @@ const namespaceKeys = <R>(root: ToolNode<R>, path: ReadonlyArray<string>): Reado
|
|||
return Array.from(node.children.keys())
|
||||
}
|
||||
|
||||
const resolve = <R>(root: ToolNode<R>, path: ReadonlyArray<string>): Definition<R> => {
|
||||
const resolve = <R>(root: ToolNode<R>, path: ReadonlyArray<string>): Tool<R> => {
|
||||
const segments = canonicalSegments(path)
|
||||
const node = lookup(root, segments)
|
||||
if (node === undefined) {
|
||||
|
|
@ -613,16 +611,16 @@ const resolve = <R>(root: ToolNode<R>, path: ReadonlyArray<string>): Definition<
|
|||
"Use search({ query }) to find available described tools.",
|
||||
])
|
||||
}
|
||||
if (node.definition === undefined) {
|
||||
if (node.tool === undefined) {
|
||||
throw new ToolRuntimeError("UnknownTool", `Tool '${segments.join(".")}' is not callable.`)
|
||||
}
|
||||
return node.definition
|
||||
return node.tool
|
||||
}
|
||||
|
||||
export type ToolRuntime<R = never> = {
|
||||
readonly root: ToolReference
|
||||
readonly calls: Array<ToolCall>
|
||||
readonly invoke: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
|
||||
readonly execute: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
|
||||
readonly search: (args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
|
||||
readonly keys: (path: ReadonlyArray<string>) => ReadonlyArray<string>
|
||||
}
|
||||
|
|
@ -676,7 +674,7 @@ export const make = <R>(
|
|||
return calls.length - 1
|
||||
}).pipe(Effect.tap((index) => hooks?.onToolCallStart?.({ index, name, input }) ?? Effect.void))
|
||||
|
||||
const invokeDefinition = (name: string, tool: Definition<R>, externalArgs: Array<unknown>) =>
|
||||
const executeTool = (name: string, tool: Tool<R>, externalArgs: Array<unknown>) =>
|
||||
Effect.gen(function* () {
|
||||
if (externalArgs.length !== 1)
|
||||
throw new ToolRuntimeError("InvalidToolInput", `Tool '${name}' expects exactly one input object.`)
|
||||
|
|
@ -688,7 +686,7 @@ export const make = <R>(
|
|||
const index = yield* recordAndObserve(name, input)
|
||||
return yield* observeEnd(
|
||||
Effect.gen(function* () {
|
||||
const raw = yield* runHost(Effect.suspend(() => tool.run(input)))
|
||||
const raw = yield* runHost(Effect.suspend(() => tool.execute(input)))
|
||||
const result = yield* Effect.try({
|
||||
try: () => decodeToolOutput(tool, raw),
|
||||
catch: () => new ToolRuntimeError("InvalidToolOutput", `Invalid output from tool '${name}'.`),
|
||||
|
|
@ -705,18 +703,18 @@ export const make = <R>(
|
|||
keys: (path) => namespaceKeys(root, path),
|
||||
search: (args) =>
|
||||
Effect.suspend(() =>
|
||||
invokeDefinition(
|
||||
executeTool(
|
||||
"search",
|
||||
searchTool,
|
||||
args.map((arg) => copyOut(copyIn(arg, "Arguments for tool 'search'"), "json")),
|
||||
),
|
||||
),
|
||||
invoke: (path, args) =>
|
||||
execute: (path, args) =>
|
||||
Effect.gen(function* () {
|
||||
const name = canonicalSegments(path).join(".")
|
||||
const externalArgs = args.map((arg) => copyOut(copyIn(arg, `Arguments for tool '${name}'`), "json"))
|
||||
const tool = resolve(root, path)
|
||||
return yield* invokeDefinition(name, tool, externalArgs)
|
||||
return yield* executeTool(name, tool, externalArgs)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue